@miphamai/cli 0.22.0 → 0.24.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.
Files changed (38) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/skills/workflows/audit.js +66 -18
  4. package/src/agent/agent-context.ts +20 -57
  5. package/src/agent/agent-experience.ts +8 -0
  6. package/src/agent/cross-session/discovery.ts +99 -0
  7. package/src/agent/cross-session/file-inbox.ts +118 -0
  8. package/src/agent/cross-session/transport.ts +17 -0
  9. package/src/agent/effectiveness-tracker.ts +149 -0
  10. package/src/agent/experience-rules.ts +188 -0
  11. package/src/agent/message-router.ts +83 -0
  12. package/src/agent/pattern-analyzer.ts +154 -0
  13. package/src/agent/sub-agent.ts +67 -0
  14. package/src/agent/types.ts +2 -0
  15. package/src/config/defaults.ts +12 -0
  16. package/src/config/loader.ts +33 -0
  17. package/src/core/credential-masker/env-filter.ts +39 -0
  18. package/src/core/credential-masker/index.ts +35 -0
  19. package/src/core/credential-masker/matcher.ts +106 -0
  20. package/src/core/credential-masker/output-scrub.ts +27 -0
  21. package/src/core/credential-masker/pipeline.ts +67 -0
  22. package/src/core/credential-masker/strategies/aws.ts +50 -0
  23. package/src/core/credential-masker/strategies/extract.ts +106 -0
  24. package/src/core/credential-masker/strategies/full.ts +15 -0
  25. package/src/core/credential-masker/strategies/jwt.ts +75 -0
  26. package/src/core/credential-masker/types.ts +16 -0
  27. package/src/core/credential-masker.ts +12 -187
  28. package/src/core/engine.ts +114 -10
  29. package/src/core/rule-engine.ts +179 -0
  30. package/src/core/session-store.ts +15 -2
  31. package/src/index.tsx +47 -2
  32. package/src/shared/types.ts +69 -5
  33. package/src/tools/agent/agent.ts +7 -1
  34. package/src/tools/agent/list-agents.ts +67 -0
  35. package/src/tools/agent/send-message.ts +21 -24
  36. package/src/tools/exec/bash.ts +67 -3
  37. package/src/tools/index.ts +2 -0
  38. package/src/ui/commands.ts +133 -0
@@ -0,0 +1,188 @@
1
+ export interface ExperienceRule {
2
+ id: string // rule-<category>-<shortHash>
3
+ type: 'mandatory' | 'warning'
4
+ condition: string
5
+ action: string
6
+ evidence: {
7
+ failureCount: number
8
+ lastFailure: string // ISO date
9
+ examples: string[]
10
+ }
11
+ category: 'timeout' | 'import' | 'search' | 'tool-params' | 'semantic'
12
+ source: 'agent-experience' | 'manual' | 'pattern-analyzer'
13
+ agentName: string
14
+ createdAt: string // ISO date
15
+ }
16
+
17
+ export interface FailureEntry {
18
+ date: string
19
+ description: string
20
+ }
21
+
22
+ export function parseFailureEntries(content: string): FailureEntry[] {
23
+ const entries: FailureEntry[] = []
24
+ const failureIdx = content.indexOf('## Failure Patterns')
25
+ if (failureIdx === -1) return entries
26
+
27
+ const afterFailure = content.slice(failureIdx)
28
+ const nextSection = afterFailure.indexOf('\n## ', '## Failure Patterns'.length)
29
+ const section = nextSection !== -1 ? afterFailure.slice(0, nextSection) : afterFailure
30
+
31
+ const lines = section.split('\n')
32
+ for (const line of lines) {
33
+ const match = line.match(/^- \[(\d{4}-\d{2}-\d{2})\]\s+(.+)/)
34
+ if (match && match[1] && match[2]) {
35
+ entries.push({ date: match[1], description: match[2].trim() })
36
+ }
37
+ }
38
+ return entries
39
+ }
40
+
41
+ export function categorize(description: string): ExperienceRule['category'] {
42
+ const lower = description.toLowerCase()
43
+ if (lower.includes('timeout')) return 'timeout'
44
+ if (/module_not_found|import|\.js/.test(lower)) return 'import'
45
+ if (/grep|token.*overflow|search.*scope/.test(lower)) return 'search'
46
+ if (/bash|command.*fail|docker|npm|pnpm|cargo|brew/.test(lower)) return 'tool-params'
47
+ return 'semantic'
48
+ }
49
+
50
+ function createRuleId(category: string, description: string): string {
51
+ // Hash the first 30 chars of description for uniqueness
52
+ const hash = description
53
+ .slice(0, 30)
54
+ .split('')
55
+ .reduce((acc, c) => {
56
+ return ((acc << 5) - acc + c.charCodeAt(0)) | 0
57
+ }, 0)
58
+ .toString(36)
59
+ .slice(-6)
60
+ return `rule-${category}-${hash}`
61
+ }
62
+
63
+ function conditionForCategory(category: string, entries: FailureEntry[]): string {
64
+ const combined = entries.map((e) => e.description).join('; ')
65
+ switch (category) {
66
+ case 'timeout':
67
+ return 'heavy CLI commands (npm/docker/pnpm)'
68
+ case 'import':
69
+ return 'ESM module imports missing .js extension'
70
+ case 'search':
71
+ return 'full-repository Grep without directory scoping'
72
+ case 'tool-params':
73
+ return combined.slice(0, 100)
74
+ default:
75
+ return combined.slice(0, 100)
76
+ }
77
+ }
78
+
79
+ function actionForCategory(category: string): string {
80
+ switch (category) {
81
+ case 'timeout':
82
+ return 'set Bash timeout ≥ 300000ms for heavy commands'
83
+ case 'import':
84
+ return 'always append .js extension to ESM relative imports'
85
+ case 'search':
86
+ return 'use Glob to narrow directory before Grep'
87
+ case 'tool-params':
88
+ return 'review and adjust tool parameters before execution'
89
+ default:
90
+ return 'verify tool parameters match known good patterns'
91
+ }
92
+ }
93
+
94
+ export class ExperienceRuleExtractor {
95
+ extract(content: string, agentName: string): ExperienceRule[] {
96
+ if (!content || !content.includes('## Failure Patterns')) return []
97
+
98
+ const entries = parseFailureEntries(content)
99
+ if (entries.length < 2) return []
100
+
101
+ // Group by category
102
+ const byCategory = new Map<string, FailureEntry[]>()
103
+ for (const entry of entries) {
104
+ const cat = categorize(entry.description)
105
+ const existing = byCategory.get(cat) || []
106
+ existing.push(entry)
107
+ byCategory.set(cat, existing)
108
+ }
109
+
110
+ const rules: ExperienceRule[] = []
111
+ for (const [category, catEntries] of byCategory) {
112
+ if (catEntries.length < 2) continue
113
+
114
+ const type: 'mandatory' | 'warning' = catEntries.length >= 3 ? 'mandatory' : 'warning'
115
+ const firstEntry = catEntries[0]
116
+ const lastEntry = catEntries[catEntries.length - 1]
117
+ if (!firstEntry || !lastEntry) continue
118
+
119
+ rules.push({
120
+ id: createRuleId(category, firstEntry.description),
121
+ type,
122
+ condition: conditionForCategory(category, catEntries),
123
+ action: actionForCategory(category),
124
+ evidence: {
125
+ failureCount: catEntries.length,
126
+ lastFailure: lastEntry.date,
127
+ examples: catEntries.slice(0, 3).map((e) => e.description),
128
+ },
129
+ category: category as ExperienceRule['category'],
130
+ source: 'agent-experience',
131
+ agentName,
132
+ createdAt: new Date().toISOString().slice(0, 10),
133
+ })
134
+ }
135
+
136
+ return rules
137
+ }
138
+
139
+ prioritize(rules: ExperienceRule[]): ExperienceRule[] {
140
+ const order: Record<string, number> = {
141
+ mandatory: 0,
142
+ warning: 1,
143
+ }
144
+ return [...rules].sort((a, b) => {
145
+ const aOrder = order[a.type] ?? 0
146
+ const bOrder = order[b.type] ?? 0
147
+ const typeDiff = aOrder - bOrder
148
+ if (typeDiff !== 0) return typeDiff
149
+ // Within same type, more failures first
150
+ return b.evidence.failureCount - a.evidence.failureCount
151
+ })
152
+ }
153
+
154
+ formatForInjection(rules: ExperienceRule[]): string {
155
+ if (rules.length === 0) return ''
156
+
157
+ const mandatory = rules.filter((r) => r.type === 'mandatory')
158
+ const warnings = rules.filter((r) => r.type === 'warning')
159
+
160
+ let output = ''
161
+
162
+ if (mandatory.length > 0) {
163
+ output += '## ⚠️ Active Mandatory Rules (learned from past failures)\n\n'
164
+ for (const r of mandatory) {
165
+ const idx = mandatory.indexOf(r) + 1
166
+ output += `${idx}. [${r.category}] ${r.condition} → ${r.action}\n`
167
+ output += ` Evidence: ${r.evidence.failureCount} failures`
168
+ if (r.evidence.lastFailure) output += `, last: ${r.evidence.lastFailure}`
169
+ output += '\n'
170
+ if (r.evidence.examples.length > 0) {
171
+ output += ` Example: ${r.evidence.examples[0]}\n`
172
+ }
173
+ output += '\n'
174
+ }
175
+ }
176
+
177
+ if (warnings.length > 0) {
178
+ output += '## ⚡ Observed Patterns (warning level)\n\n'
179
+ for (const r of warnings) {
180
+ const idx = warnings.indexOf(r) + 1
181
+ output += `${idx}. [${r.category}] ${r.condition} → ${r.action}\n`
182
+ output += ` Evidence: ${r.evidence.failureCount} occurrences\n\n`
183
+ }
184
+ }
185
+
186
+ return output.trim()
187
+ }
188
+ }
@@ -0,0 +1,83 @@
1
+ import { getMessageBus } from './message-bus'
2
+ import { getFileInboxTransport } from './cross-session/file-inbox'
3
+ import { discoverSessions, createSessionInfo } from './cross-session/discovery'
4
+ import type { AgentMessage } from './message-bus'
5
+
6
+ export interface RouteResult {
7
+ success: boolean
8
+ routedTo: 'bus' | 'inbox' | 'unknown'
9
+ error?: string
10
+ messageId?: string
11
+ }
12
+
13
+ /**
14
+ * MessageRouter decides how to deliver a message based on the recipient.
15
+ *
16
+ * - "main" or background task IDs → in-memory AgentMessageBus (same process)
17
+ * - Session IDs matching discoverable sessions → file inbox (cross-session)
18
+ * - Unknown → error
19
+ */
20
+ export class MessageRouter {
21
+ /**
22
+ * Route a message to the appropriate transport.
23
+ */
24
+ async route(from: string, to: string, summary: string, message: string): Promise<RouteResult> {
25
+ // Same-process routing: "main" or background task IDs
26
+ if (to === 'main' || to.startsWith('bg-') || to.startsWith('sub-agent-')) {
27
+ try {
28
+ const bus = getMessageBus()
29
+ const msgId = bus.post(from, to, summary, message)
30
+ return { success: true, routedTo: 'bus', messageId: msgId }
31
+ } catch (err) {
32
+ return { success: false, routedTo: 'bus', error: String(err) }
33
+ }
34
+ }
35
+
36
+ // Cross-session routing
37
+ const sessions = discoverSessions()
38
+ const targetSession = sessions.find((s) => s.id === to)
39
+
40
+ if (!targetSession) {
41
+ return {
42
+ success: false,
43
+ routedTo: 'unknown',
44
+ error: `No active session found with ID "${to}". Use ListAgents to discover available sessions.`,
45
+ }
46
+ }
47
+
48
+ // Get sender info
49
+ const senderSession = createSessionInfo(from, from)
50
+
51
+ try {
52
+ const transport = getFileInboxTransport()
53
+ const msg: AgentMessage = {
54
+ id: `xmsg-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
55
+ from,
56
+ to,
57
+ summary: summary.slice(0, 200),
58
+ message,
59
+ timestamp: new Date(),
60
+ read: false,
61
+ type: 'message',
62
+ }
63
+
64
+ const delivered = await transport.send(senderSession, to, msg)
65
+ if (!delivered) {
66
+ return { success: false, routedTo: 'inbox', error: 'Failed to write message to inbox.' }
67
+ }
68
+
69
+ return { success: true, routedTo: 'inbox', messageId: msg.id }
70
+ } catch (err) {
71
+ return { success: false, routedTo: 'inbox', error: String(err) }
72
+ }
73
+ }
74
+ }
75
+
76
+ let _router: MessageRouter | null = null
77
+
78
+ export function getMessageRouter(): MessageRouter {
79
+ if (!_router) {
80
+ _router = new MessageRouter()
81
+ }
82
+ return _router
83
+ }
@@ -0,0 +1,154 @@
1
+ import { readdirSync, existsSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { homedir } from 'node:os'
4
+ import { AgentExperience } from './agent-experience.js'
5
+ import { parseFailureEntries, categorize, type ExperienceRule } from './experience-rules.js'
6
+ import type { ToolRule } from '../core/rule-engine.js'
7
+
8
+ export interface Pattern {
9
+ id: string
10
+ category: 'timeout' | 'import' | 'search' | 'tool-params' | 'semantic'
11
+ agentName: string
12
+ frequency: number
13
+ confidence: 'high' | 'medium' | 'low'
14
+ examples: string[]
15
+ firstSeen: string
16
+ lastSeen: string
17
+ }
18
+
19
+ export class PatternAnalyzer {
20
+ analyzeAgent(agentName: string, baseDir?: string): Pattern[] {
21
+ const dir = baseDir || join(homedir(), '.mipham', 'agent-memory')
22
+ const exp = new AgentExperience(agentName, dir)
23
+ const content = exp.getExperience()
24
+ if (!content) return []
25
+
26
+ const allEntries = parseFailureEntries(content)
27
+
28
+ // Group by category
29
+ const byCategory = new Map<string, { descriptions: string[]; dates: string[] }>()
30
+ for (const entry of allEntries) {
31
+ const cat = categorize(entry.description)
32
+ const existing = byCategory.get(cat) || { descriptions: [], dates: [] }
33
+ existing.descriptions.push(entry.description)
34
+ existing.dates.push(entry.date)
35
+ byCategory.set(cat, existing)
36
+ }
37
+
38
+ const patterns: Pattern[] = []
39
+ for (const [category, data] of byCategory) {
40
+ if (data.descriptions.length < 3) continue
41
+
42
+ patterns.push({
43
+ id: `pattern-${category}-${agentName}`,
44
+ category: category as Pattern['category'],
45
+ agentName,
46
+ frequency: data.descriptions.length,
47
+ confidence: data.descriptions.length >= 5 ? 'high' : 'medium',
48
+ examples: data.descriptions.slice(0, 5),
49
+ firstSeen: data.dates[0] ?? '',
50
+ lastSeen: data.dates[data.dates.length - 1] ?? '',
51
+ })
52
+ }
53
+
54
+ return patterns
55
+ }
56
+
57
+ analyzeAllAgents(baseDir?: string): Pattern[] {
58
+ const dir = baseDir || join(homedir(), '.mipham', 'agent-memory')
59
+ if (!existsSync(dir)) return []
60
+
61
+ let agents: string[]
62
+ try {
63
+ agents = readdirSync(dir, { withFileTypes: true })
64
+ .filter((d) => d.isDirectory())
65
+ .map((d) => d.name)
66
+ } catch {
67
+ return []
68
+ }
69
+
70
+ const allPatterns: Pattern[] = []
71
+ for (const agent of agents) {
72
+ allPatterns.push(...this.analyzeAgent(agent, baseDir))
73
+ }
74
+ return allPatterns
75
+ }
76
+
77
+ toRule(pattern: Pattern): ExperienceRule {
78
+ return {
79
+ id: `${pattern.id}-${Date.now().toString(36)}`,
80
+ type: pattern.frequency >= 3 ? 'mandatory' : 'warning',
81
+ condition: this._conditionForCategory(pattern),
82
+ action: this._actionForCategory(pattern.category),
83
+ evidence: {
84
+ failureCount: pattern.frequency,
85
+ lastFailure: pattern.lastSeen,
86
+ examples: pattern.examples.slice(0, 3),
87
+ },
88
+ category: pattern.category,
89
+ source: 'pattern-analyzer',
90
+ agentName: pattern.agentName,
91
+ createdAt: new Date().toISOString().slice(0, 10),
92
+ }
93
+ }
94
+
95
+ toToolRule(pattern: Pattern): ToolRule {
96
+ const toolNameMap: Record<string, string> = {
97
+ timeout: 'Bash',
98
+ 'tool-params': 'Bash',
99
+ import: 'Write',
100
+ search: 'Grep',
101
+ semantic: 'Bash',
102
+ }
103
+
104
+ return {
105
+ id: pattern.id,
106
+ toolName: toolNameMap[pattern.category] || 'Bash',
107
+ category: pattern.category,
108
+ match: (p: Record<string, unknown>): boolean => {
109
+ if (pattern.category === 'timeout') {
110
+ const cmd = String(p.command ?? '')
111
+ const heavy = /npm|docker|pnpm|cargo|brew|install|build/.test(cmd)
112
+ if (!heavy) return false
113
+ const timeout = (p as Record<string, unknown>).timeout as number | undefined
114
+ return !timeout || timeout < 300_000
115
+ }
116
+ return true
117
+ },
118
+ fix: (p: Record<string, unknown>) => ({
119
+ modified: pattern.category === 'timeout' ? { ...p, timeout: 300_000 } : p,
120
+ warning: `🤖 [auto-rule] ${pattern.category}: ${pattern.frequency} 次同类失败 — ${this._actionForCategory(pattern.category)}`,
121
+ }),
122
+ source: 'pattern-analyzer',
123
+ enabled: true,
124
+ }
125
+ }
126
+
127
+ // ── Private helpers ──
128
+
129
+ private _conditionForCategory(pattern: Pattern): string {
130
+ switch (pattern.category) {
131
+ case 'timeout':
132
+ return 'heavy CLI commands (npm/docker/pnpm/cargo)'
133
+ case 'import':
134
+ return 'ESM module imports missing .js extension'
135
+ case 'search':
136
+ return 'full-repository search without directory scoping'
137
+ default:
138
+ return pattern.examples[0]?.slice(0, 100) || 'unknown condition'
139
+ }
140
+ }
141
+
142
+ private _actionForCategory(category: string): string {
143
+ switch (category) {
144
+ case 'timeout':
145
+ return 'set Bash timeout ≥ 300000ms for heavy commands'
146
+ case 'import':
147
+ return 'append .js extension to ESM relative imports'
148
+ case 'search':
149
+ return 'use Glob to narrow directory before Grep'
150
+ default:
151
+ return 'review and adjust tool parameters before execution'
152
+ }
153
+ }
154
+ }
@@ -7,6 +7,16 @@ import { getMessageBus } from './message-bus'
7
7
  import type { HookEngine } from '../core/hooks'
8
8
  import type { PermissionSystem } from '../core/permission'
9
9
  import { AgentExperience } from './agent-experience'
10
+ import { PatternAnalyzer } from './pattern-analyzer.js'
11
+ import type { ExperienceRuleEngine } from '../core/rule-engine.js'
12
+
13
+ // Singleton instances (created lazily)
14
+ let _patternAnalyzer: PatternAnalyzer | undefined
15
+
16
+ function getPatternAnalyzer(): PatternAnalyzer {
17
+ if (!_patternAnalyzer) _patternAnalyzer = new PatternAnalyzer()
18
+ return _patternAnalyzer
19
+ }
10
20
 
11
21
  const TYPE_SYSTEM_PROMPTS: Record<SubAgentType, string> = {
12
22
  general: 'You are a focused sub-agent. Complete the assigned task thoroughly and return results.',
@@ -32,6 +42,7 @@ export class SubAgent {
32
42
  private toolRegistry: Map<string, ToolDefinition>,
33
43
  private permission?: PermissionSystem,
34
44
  private hookEngine?: HookEngine,
45
+ private ruleEngine?: ExperienceRuleEngine,
35
46
  ) {}
36
47
 
37
48
  /**
@@ -85,6 +96,24 @@ export class SubAgent {
85
96
  options.agentDef,
86
97
  )
87
98
  }
99
+
100
+ // CRSI: trigger pattern analysis after each agent execution
101
+ // Gated by crsi.autoPatternAnalysis feature flag
102
+ if (options.autoPatternAnalysis !== false) {
103
+ try {
104
+ const analyzer = getPatternAnalyzer()
105
+ const agentName = options.agentDef?.name || agentType
106
+ const patterns = analyzer.analyzeAgent(agentName)
107
+ if (patterns.length > 0 && this.ruleEngine) {
108
+ for (const pattern of patterns) {
109
+ const toolRule = analyzer.toToolRule(pattern)
110
+ this.ruleEngine.register(toolRule)
111
+ }
112
+ }
113
+ } catch {
114
+ // Pattern analysis failure never blocks agent execution
115
+ }
116
+ }
88
117
  })
89
118
 
90
119
  return `[background-task:${taskId}]`
@@ -97,6 +126,25 @@ export class SubAgent {
97
126
  await this.hookEngine.executeSubagentStop(agentType, description, 'sub-agent', true, result)
98
127
  }
99
128
  this.logSuccessExperience(agentType, description, result, options.agentDef)
129
+
130
+ // CRSI: trigger pattern analysis after successful sync execution
131
+ // Gated by crsi.autoPatternAnalysis feature flag
132
+ if (options.autoPatternAnalysis !== false) {
133
+ try {
134
+ const analyzer = getPatternAnalyzer()
135
+ const agentName = options.agentDef?.name || agentType
136
+ const patterns = analyzer.analyzeAgent(agentName)
137
+ if (patterns.length > 0 && this.ruleEngine) {
138
+ for (const pattern of patterns) {
139
+ const toolRule = analyzer.toToolRule(pattern)
140
+ this.ruleEngine.register(toolRule)
141
+ }
142
+ }
143
+ } catch {
144
+ // Pattern analysis failure never blocks agent execution
145
+ }
146
+ }
147
+
100
148
  return result
101
149
  } catch (err) {
102
150
  if (this.hookEngine) {
@@ -109,6 +157,25 @@ export class SubAgent {
109
157
  )
110
158
  }
111
159
  this.logFailureExperience(agentType, description, String(err), options.agentDef)
160
+
161
+ // CRSI: trigger pattern analysis after failed sync execution (failures produce patterns too)
162
+ // Gated by crsi.autoPatternAnalysis feature flag
163
+ if (options.autoPatternAnalysis !== false) {
164
+ try {
165
+ const analyzer = getPatternAnalyzer()
166
+ const agentName = options.agentDef?.name || agentType
167
+ const patterns = analyzer.analyzeAgent(agentName)
168
+ if (patterns.length > 0 && this.ruleEngine) {
169
+ for (const pattern of patterns) {
170
+ const toolRule = analyzer.toToolRule(pattern)
171
+ this.ruleEngine.register(toolRule)
172
+ }
173
+ }
174
+ } catch {
175
+ // Pattern analysis failure never blocks agent execution
176
+ }
177
+ }
178
+
112
179
  throw err
113
180
  }
114
181
  }
@@ -46,4 +46,6 @@ export interface SubAgentOptions {
46
46
  onProgress?: (chunk: string) => void
47
47
  /** When set, tool executions use this path as cwd (git worktree isolation). */
48
48
  worktreePath?: string
49
+ /** CRSI: when false, skip pattern analysis after agent execution. Default true. */
50
+ autoPatternAnalysis?: boolean
49
51
  }
@@ -3,6 +3,7 @@ import type {
3
3
  InferenceHookConfig,
4
4
  CredentialMaskingConfig,
5
5
  BackgroundAgentConfig,
6
+ CrossSessionConfig,
6
7
  } from '../shared/index.ts'
7
8
  import { DEFAULT_PROVIDERS } from '../shared/index.ts'
8
9
  import { PACKAGE_VERSION } from '../shared/index.ts'
@@ -21,6 +22,12 @@ export const DEFAULT_CONFIG: MiphamConfig = {
21
22
  mcp: { oauthEnabled: true },
22
23
  context: { useRealTokenizer: true, adaptiveThresholds: true },
23
24
  },
25
+ crsi: {
26
+ ruleInjection: true,
27
+ preToolHook: true,
28
+ autoPatternAnalysis: true,
29
+ autoRuleManagement: true,
30
+ },
24
31
  }
25
32
 
26
33
  export const DEFAULT_INFERENCE_HOOK_CONFIG: InferenceHookConfig = {
@@ -51,3 +58,8 @@ export const DEFAULT_BACKGROUND_AGENT_CONFIG: BackgroundAgentConfig = {
51
58
  auto_worktree: true,
52
59
  commit_coauthors: true,
53
60
  }
61
+
62
+ export const DEFAULT_CROSS_SESSION_CONFIG: CrossSessionConfig = {
63
+ crossSessionInbound: 'ask',
64
+ dialogExpiry: 300,
65
+ }
@@ -17,12 +17,14 @@ import type {
17
17
  InferenceHookConfig,
18
18
  CredentialMaskingConfig,
19
19
  BackgroundAgentConfig,
20
+ CrossSessionConfig,
20
21
  } from '../shared/index.ts'
21
22
  import {
22
23
  DEFAULT_CONFIG,
23
24
  DEFAULT_INFERENCE_HOOK_CONFIG,
24
25
  DEFAULT_CREDENTIAL_MASKING_CONFIG,
25
26
  DEFAULT_BACKGROUND_AGENT_CONFIG,
27
+ DEFAULT_CROSS_SESSION_CONFIG,
26
28
  } from './defaults'
27
29
 
28
30
  const MIPHAM_HOME = join(homedir(), '.mipham')
@@ -376,3 +378,34 @@ export function loadBackgroundAgentConfig(cwd: string = process.cwd()): Backgrou
376
378
 
377
379
  return merged
378
380
  }
381
+
382
+ /**
383
+ * Load cross-session messaging configuration from the same config sources.
384
+ * Merges project-level over user-level. Returns defaults if no section present.
385
+ */
386
+ export function loadCrossSessionConfig(cwd: string = process.cwd()): CrossSessionConfig {
387
+ const configPath = join(cwd, '.mipham', 'config.yml')
388
+ const userConfigPath = join(MIPHAM_HOME, 'config.yml')
389
+
390
+ let merged = { ...DEFAULT_CROSS_SESSION_CONFIG }
391
+
392
+ const paths = [userConfigPath, configPath] // project wins (loaded last)
393
+ for (const path of paths) {
394
+ try {
395
+ if (!existsSync(path)) continue
396
+ const raw = readFileSync(path, 'utf-8')
397
+ const parsed = parseYaml(raw) as Record<string, unknown>
398
+ const section = parsed.cross_session as Partial<CrossSessionConfig> | undefined
399
+ if (section) {
400
+ merged = {
401
+ crossSessionInbound: section.crossSessionInbound ?? merged.crossSessionInbound,
402
+ dialogExpiry: section.dialogExpiry ?? merged.dialogExpiry,
403
+ }
404
+ }
405
+ } catch {
406
+ // Silently skip malformed configs
407
+ }
408
+ }
409
+
410
+ return merged
411
+ }
@@ -0,0 +1,39 @@
1
+ import { CREDENTIAL_SENTINEL } from './types'
2
+ import type { CredentialMaskingConfig } from '../../shared/index.ts'
3
+
4
+ /**
5
+ * Filter sensitive environment variables before spawning a subprocess.
6
+ * Returns a sanitized copy of process.env with matching vars removed.
7
+ */
8
+ export function filterEnv(
9
+ env: Record<string, string | undefined>,
10
+ config: CredentialMaskingConfig,
11
+ ): Record<string, string | undefined> {
12
+ if (!config.enabled || !config.env_filter.enabled) return { ...env }
13
+
14
+ const filtered: Record<string, string | undefined> = {}
15
+ for (const [key, value] of Object.entries(env)) {
16
+ let blocked = false
17
+ for (const pattern of config.env_filter.patterns) {
18
+ try {
19
+ // Strip (?i) inline flags — JS uses the 'i' flag instead
20
+ const clean = pattern.replace(/^\(\?i\)/, '')
21
+ const regex = new RegExp(clean, 'i')
22
+ if (regex.test(key)) {
23
+ blocked = true
24
+ break
25
+ }
26
+ } catch {
27
+ // Invalid regex — skip
28
+ }
29
+ }
30
+ if (blocked) {
31
+ // Replace with sentinel so tools can detect they're filtered
32
+ filtered[key] = CREDENTIAL_SENTINEL
33
+ } else {
34
+ filtered[key] = value
35
+ }
36
+ }
37
+
38
+ return filtered
39
+ }
@@ -0,0 +1,35 @@
1
+ export { CREDENTIAL_SENTINEL } from './types'
2
+ export type { MaskingStrategy } from './types'
3
+
4
+ export { MaskingPipeline, getPipeline, maskContent } from './pipeline'
5
+ export { matchCredentialFile, matchPath, globToRegex } from './matcher'
6
+ export { filterEnv } from './env-filter'
7
+ export { maskOutput } from './output-scrub'
8
+
9
+ // Strategies
10
+ export { FullMaskingStrategy } from './strategies/full'
11
+ export { ExtractMaskingStrategy } from './strategies/extract'
12
+ export { JwtMaskingStrategy } from './strategies/jwt'
13
+ export { AwsMaskingStrategy } from './strategies/aws'
14
+
15
+ import { getPipeline } from './pipeline'
16
+ import { FullMaskingStrategy } from './strategies/full'
17
+ import { ExtractMaskingStrategy } from './strategies/extract'
18
+ import { JwtMaskingStrategy } from './strategies/jwt'
19
+ import { AwsMaskingStrategy } from './strategies/aws'
20
+
21
+ /**
22
+ * Initialize the masking pipeline with all built-in strategies.
23
+ * Call once at startup. Safe to call multiple times (idempotent).
24
+ */
25
+ let _initialized = false
26
+
27
+ export function initializePipeline(): void {
28
+ if (_initialized) return
29
+ const pipeline = getPipeline()
30
+ pipeline.register(new FullMaskingStrategy())
31
+ pipeline.register(new ExtractMaskingStrategy())
32
+ pipeline.register(new JwtMaskingStrategy())
33
+ pipeline.register(new AwsMaskingStrategy())
34
+ _initialized = true
35
+ }