@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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/skills/workflows/audit.js +66 -18
- package/src/agent/agent-context.ts +20 -57
- package/src/agent/agent-experience.ts +8 -0
- package/src/agent/cross-session/discovery.ts +99 -0
- package/src/agent/cross-session/file-inbox.ts +118 -0
- package/src/agent/cross-session/transport.ts +17 -0
- package/src/agent/effectiveness-tracker.ts +149 -0
- package/src/agent/experience-rules.ts +188 -0
- package/src/agent/message-router.ts +83 -0
- package/src/agent/pattern-analyzer.ts +154 -0
- package/src/agent/sub-agent.ts +67 -0
- package/src/agent/types.ts +2 -0
- package/src/config/defaults.ts +12 -0
- package/src/config/loader.ts +33 -0
- package/src/core/credential-masker/env-filter.ts +39 -0
- package/src/core/credential-masker/index.ts +35 -0
- package/src/core/credential-masker/matcher.ts +106 -0
- package/src/core/credential-masker/output-scrub.ts +27 -0
- package/src/core/credential-masker/pipeline.ts +67 -0
- package/src/core/credential-masker/strategies/aws.ts +50 -0
- package/src/core/credential-masker/strategies/extract.ts +106 -0
- package/src/core/credential-masker/strategies/full.ts +15 -0
- package/src/core/credential-masker/strategies/jwt.ts +75 -0
- package/src/core/credential-masker/types.ts +16 -0
- package/src/core/credential-masker.ts +12 -187
- package/src/core/engine.ts +114 -10
- package/src/core/rule-engine.ts +179 -0
- package/src/core/session-store.ts +15 -2
- package/src/index.tsx +47 -2
- package/src/shared/types.ts +69 -5
- package/src/tools/agent/agent.ts +7 -1
- package/src/tools/agent/list-agents.ts +67 -0
- package/src/tools/agent/send-message.ts +21 -24
- package/src/tools/exec/bash.ts +67 -3
- package/src/tools/index.ts +2 -0
- package/src/ui/commands.ts +133 -0
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Requirements: **Bun 1.2+** (recommended) or **Node.js 22+**
|
|
|
16
16
|
## Features
|
|
17
17
|
|
|
18
18
|
- **9 AI Providers** — Anthropic Claude · OpenAI GPT · DeepSeek · Kimi (Moonshot) · Google Gemini · Doubao 豆包 · Tencent Hunyuan 混元 · Qwen 通义千问 · MiphamAI
|
|
19
|
-
- **66 Slash Commands** — Interactive command picker (↑↓ Enter Esc),
|
|
19
|
+
- **66 Slash Commands** — Interactive command picker (↑↓ Enter Esc), intuitive command interface, zero re-learning
|
|
20
20
|
- **16 Built-in Tools** — File ops, shell execution, agent dispatch, MCP integration, web search
|
|
21
21
|
- **14 Skills + Marketplace** — 11 standard + 3 Mipham-exclusive skills. `/browse-skills` `/install-skill` community marketplace
|
|
22
22
|
- **Self-Update** — `mipham update` / `/upgrade` — one command to check, backup config, upgrade, restore. API keys preserved
|
package/package.json
CHANGED
|
@@ -10,37 +10,85 @@ export const meta = {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
phase('Scope')
|
|
13
|
-
const targets =
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
const targets =
|
|
14
|
+
args.targets ||
|
|
15
|
+
(
|
|
16
|
+
await agent(
|
|
17
|
+
'List all source files that need security auditing. Return { files: [{ path, reason }] }',
|
|
18
|
+
{
|
|
19
|
+
schema: {
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
files: {
|
|
23
|
+
type: 'array',
|
|
24
|
+
items: {
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: { path: { type: 'string' }, reason: { type: 'string' } },
|
|
27
|
+
required: ['path', 'reason'],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
required: ['files'],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
)
|
|
35
|
+
).files
|
|
17
36
|
|
|
18
37
|
log(`Auditing ${targets.length} files`)
|
|
19
38
|
|
|
20
39
|
phase('Audit')
|
|
21
|
-
const raw =
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
{
|
|
25
|
-
|
|
40
|
+
const raw = await pipeline(targets, (t) =>
|
|
41
|
+
agent(
|
|
42
|
+
`Security audit ${t.path}: injection, auth, crypto, secrets, input validation. Return { findings: [{ severity, file, line, summary }] }`,
|
|
43
|
+
{
|
|
44
|
+
label: `audit:${t.path}`,
|
|
45
|
+
schema: {
|
|
46
|
+
type: 'object',
|
|
47
|
+
properties: {
|
|
48
|
+
findings: {
|
|
49
|
+
type: 'array',
|
|
50
|
+
items: {
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties: {
|
|
53
|
+
severity: { type: 'string', enum: ['critical', 'high', 'medium', 'low'] },
|
|
54
|
+
file: { type: 'string' },
|
|
55
|
+
line: { type: 'number' },
|
|
56
|
+
summary: { type: 'string' },
|
|
57
|
+
},
|
|
58
|
+
required: ['severity', 'file', 'summary'],
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
required: ['findings'],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
),
|
|
26
66
|
)
|
|
27
|
-
const findings = raw.flatMap(r => (r && r.findings) || [])
|
|
67
|
+
const findings = raw.flatMap((r) => (r && r.findings) || [])
|
|
28
68
|
|
|
29
69
|
log(`Found ${findings.length} potential issues`)
|
|
30
70
|
|
|
31
71
|
phase('Verify')
|
|
32
72
|
const verified = await parallel(
|
|
33
|
-
findings.map(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
73
|
+
findings.map(
|
|
74
|
+
(f) => () =>
|
|
75
|
+
verify(f, {
|
|
76
|
+
mode: 'adversarial',
|
|
77
|
+
skeptics: 3,
|
|
78
|
+
threshold: 2,
|
|
79
|
+
schema: {
|
|
80
|
+
type: 'object',
|
|
81
|
+
properties: { real: { type: 'boolean' }, reason: { type: 'string' } },
|
|
82
|
+
required: ['real', 'reason'],
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
40
85
|
),
|
|
41
86
|
)
|
|
42
87
|
|
|
43
|
-
const confirmed = verified
|
|
88
|
+
const confirmed = verified
|
|
89
|
+
.filter(Boolean)
|
|
90
|
+
.filter((v) => v.survives)
|
|
91
|
+
.map((v) => v.finding)
|
|
44
92
|
log(`${confirmed.length}/${findings.length} findings verified`)
|
|
45
93
|
|
|
46
94
|
phase('Report')
|
|
@@ -5,7 +5,9 @@ import { homedir } from 'node:os'
|
|
|
5
5
|
import { ContextManager } from '../core/context'
|
|
6
6
|
import type { ToolDefinition } from '../shared/index.ts'
|
|
7
7
|
import type { AgentDefinition } from './types'
|
|
8
|
+
import type { CrsiConfig } from '../shared/index.ts'
|
|
8
9
|
import { AgentExperience } from './agent-experience'
|
|
10
|
+
import { ExperienceRuleExtractor } from './experience-rules.js'
|
|
9
11
|
|
|
10
12
|
export interface AgentContextResult {
|
|
11
13
|
context: ContextManager
|
|
@@ -15,8 +17,14 @@ export interface AgentContextResult {
|
|
|
15
17
|
/**
|
|
16
18
|
* Load agent memory files from the appropriate scope directory.
|
|
17
19
|
* Returns combined content for injection into the system prompt.
|
|
20
|
+
*
|
|
21
|
+
* @param crsiRuleInjection — when false, skip experience rule extraction (CRSI feature flag).
|
|
18
22
|
*/
|
|
19
|
-
function loadAgentMemory(
|
|
23
|
+
function loadAgentMemory(
|
|
24
|
+
agentName: string,
|
|
25
|
+
scope: 'user' | 'project' | 'local',
|
|
26
|
+
crsiRuleInjection = true,
|
|
27
|
+
): string {
|
|
20
28
|
let memoryDir: string
|
|
21
29
|
const home = homedir()
|
|
22
30
|
|
|
@@ -56,63 +64,16 @@ function loadAgentMemory(agentName: string, scope: 'user' | 'project' | 'local')
|
|
|
56
64
|
}
|
|
57
65
|
}
|
|
58
66
|
|
|
59
|
-
// Load auto-accumulated experience (always from user scope)
|
|
60
|
-
|
|
61
|
-
const experienceContent = exp.getExperience()
|
|
62
|
-
|
|
67
|
+
// Load auto-accumulated experience rules (always from user scope)
|
|
68
|
+
// Gated by crsi.ruleInjection feature flag
|
|
63
69
|
let experienceMemory = ''
|
|
64
|
-
if (
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
const failureIdx = lines.findIndex((l) => l.startsWith('## Failure Patterns'))
|
|
69
|
-
|
|
70
|
-
// Header: content before the first section
|
|
71
|
-
const firstSection = Math.min(
|
|
72
|
-
successIdx !== -1 ? successIdx : Infinity,
|
|
73
|
-
failureIdx !== -1 ? failureIdx : Infinity,
|
|
74
|
-
statsIdx !== -1 ? statsIdx : Infinity,
|
|
75
|
-
)
|
|
76
|
-
const header = lines.slice(0, firstSection === Infinity ? 3 : firstSection)
|
|
77
|
-
|
|
78
|
-
// Last 5 success entries (summary line only, not detail)
|
|
79
|
-
const successLines: string[] = []
|
|
80
|
-
if (successIdx !== -1) {
|
|
81
|
-
const endIdx = Math.min(
|
|
82
|
-
failureIdx !== -1 && failureIdx > successIdx ? failureIdx : Infinity,
|
|
83
|
-
statsIdx !== -1 && statsIdx > successIdx ? statsIdx : Infinity,
|
|
84
|
-
)
|
|
85
|
-
successLines.push(
|
|
86
|
-
...lines
|
|
87
|
-
.slice(successIdx + 1, endIdx === Infinity ? undefined : endIdx)
|
|
88
|
-
.filter((l) => l.startsWith('- ['))
|
|
89
|
-
.slice(-5),
|
|
90
|
-
)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Last 3 failure entries (summary line only, not detail)
|
|
94
|
-
const failureLines: string[] = []
|
|
95
|
-
if (failureIdx !== -1) {
|
|
96
|
-
const endIdx = statsIdx !== -1 && statsIdx > failureIdx ? statsIdx : Infinity
|
|
97
|
-
failureLines.push(
|
|
98
|
-
...lines
|
|
99
|
-
.slice(failureIdx + 1, endIdx === Infinity ? undefined : endIdx)
|
|
100
|
-
.filter((l) => l.startsWith('- ['))
|
|
101
|
-
.slice(-3),
|
|
102
|
-
)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Stats section (header + stat line)
|
|
106
|
-
const stats = statsIdx !== -1 ? lines.slice(statsIdx, statsIdx + 3) : []
|
|
70
|
+
if (crsiRuleInjection) {
|
|
71
|
+
const exp = new AgentExperience(agentName)
|
|
72
|
+
const extractor = new ExperienceRuleExtractor()
|
|
73
|
+
const rules = extractor.prioritize(exp.getRules())
|
|
107
74
|
|
|
108
|
-
if (
|
|
109
|
-
experienceMemory =
|
|
110
|
-
'## Agent Experience',
|
|
111
|
-
...header.filter((l) => l.trim()),
|
|
112
|
-
...successLines,
|
|
113
|
-
...failureLines,
|
|
114
|
-
...stats,
|
|
115
|
-
].join('\n')
|
|
75
|
+
if (rules.length > 0) {
|
|
76
|
+
experienceMemory = extractor.formatForInjection(rules)
|
|
116
77
|
}
|
|
117
78
|
}
|
|
118
79
|
|
|
@@ -139,6 +100,7 @@ export function createAgentContext(
|
|
|
139
100
|
agentDef: AgentDefinition,
|
|
140
101
|
toolRegistry: Map<string, ToolDefinition>,
|
|
141
102
|
contextWindow?: number,
|
|
103
|
+
crsiConfig?: Partial<CrsiConfig>,
|
|
142
104
|
): AgentContextResult {
|
|
143
105
|
// Create isolated context
|
|
144
106
|
const context = new ContextManager({
|
|
@@ -149,7 +111,8 @@ export function createAgentContext(
|
|
|
149
111
|
// Build system prompt with optional agent memory
|
|
150
112
|
let systemPrompt = agentDef.systemPrompt
|
|
151
113
|
if (agentDef.memory) {
|
|
152
|
-
const
|
|
114
|
+
const ruleInjection = crsiConfig?.ruleInjection !== false
|
|
115
|
+
const memory = loadAgentMemory(agentDef.name, agentDef.memory, ruleInjection)
|
|
153
116
|
if (memory) {
|
|
154
117
|
systemPrompt = `${systemPrompt}\n\n---\n\n${memory}`
|
|
155
118
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync, existsSync, unlinkSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
+
import { ExperienceRuleExtractor, type ExperienceRule } from './experience-rules.js'
|
|
3
4
|
|
|
4
5
|
const MAX_EXPERIENCES = 20
|
|
5
6
|
|
|
@@ -46,6 +47,13 @@ export class AgentExperience {
|
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
getRules(): ExperienceRule[] {
|
|
51
|
+
const content = this.getExperience()
|
|
52
|
+
if (!content) return []
|
|
53
|
+
const extractor = new ExperienceRuleExtractor()
|
|
54
|
+
return extractor.extract(content, this.agentName)
|
|
55
|
+
}
|
|
56
|
+
|
|
49
57
|
private appendToSection(section: string, entry: string, type: 'success' | 'failure'): void {
|
|
50
58
|
mkdirSync(this.expDir, { recursive: true })
|
|
51
59
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readdirSync,
|
|
3
|
+
readFileSync,
|
|
4
|
+
writeFileSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
unlinkSync,
|
|
8
|
+
} from 'node:fs'
|
|
9
|
+
import { join } from 'node:path'
|
|
10
|
+
import { homedir, hostname } from 'node:os'
|
|
11
|
+
import type { SessionInfo } from '../../shared/types'
|
|
12
|
+
|
|
13
|
+
const MIPHAM_HOME = join(homedir(), '.mipham')
|
|
14
|
+
const ACTIVE_SESSIONS_DIR = join(MIPHAM_HOME, '.active-sessions')
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Register the current session as active.
|
|
18
|
+
* Writes a SessionInfo JSON file + updates mtime for heartbeat.
|
|
19
|
+
*/
|
|
20
|
+
export function registerActiveSession(info: SessionInfo): void {
|
|
21
|
+
mkdirSync(ACTIVE_SESSIONS_DIR, { recursive: true })
|
|
22
|
+
const filePath = join(ACTIVE_SESSIONS_DIR, `${info.id}.json`)
|
|
23
|
+
writeFileSync(filePath, JSON.stringify(info, null, 2), 'utf-8')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Update heartbeat (mtime) for an active session.
|
|
28
|
+
*/
|
|
29
|
+
export function heartbeatSession(sessionId: string): void {
|
|
30
|
+
const filePath = join(ACTIVE_SESSIONS_DIR, `${sessionId}.json`)
|
|
31
|
+
if (existsSync(filePath)) {
|
|
32
|
+
// Touch the file by rewriting it
|
|
33
|
+
const raw = readFileSync(filePath, 'utf-8')
|
|
34
|
+
writeFileSync(filePath, raw, 'utf-8') // updates mtime
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Unregister a session (called on shutdown).
|
|
40
|
+
*/
|
|
41
|
+
export function unregisterSession(sessionId: string): void {
|
|
42
|
+
const filePath = join(ACTIVE_SESSIONS_DIR, `${sessionId}.json`)
|
|
43
|
+
try {
|
|
44
|
+
if (existsSync(filePath)) unlinkSync(filePath)
|
|
45
|
+
} catch {
|
|
46
|
+
// Best-effort
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Discover all active sessions.
|
|
52
|
+
* Reads ~/.mipham/.active-sessions/ directory.
|
|
53
|
+
*/
|
|
54
|
+
export function discoverSessions(): SessionInfo[] {
|
|
55
|
+
if (!existsSync(ACTIVE_SESSIONS_DIR)) return []
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const files = readdirSync(ACTIVE_SESSIONS_DIR).filter((f) => f.endsWith('.json'))
|
|
59
|
+
|
|
60
|
+
const sessions: SessionInfo[] = []
|
|
61
|
+
for (const file of files) {
|
|
62
|
+
try {
|
|
63
|
+
const raw = readFileSync(join(ACTIVE_SESSIONS_DIR, file), 'utf-8')
|
|
64
|
+
const info = JSON.parse(raw) as SessionInfo
|
|
65
|
+
sessions.push(info)
|
|
66
|
+
} catch {
|
|
67
|
+
// Skip corrupt files
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Sort by most recently active
|
|
72
|
+
sessions.sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime())
|
|
73
|
+
return sessions
|
|
74
|
+
} catch {
|
|
75
|
+
return []
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Create a SessionInfo for the current process.
|
|
81
|
+
*/
|
|
82
|
+
export function createSessionInfo(
|
|
83
|
+
sessionId: string,
|
|
84
|
+
name: string,
|
|
85
|
+
cwd?: string,
|
|
86
|
+
provider?: string,
|
|
87
|
+
model?: string,
|
|
88
|
+
): SessionInfo {
|
|
89
|
+
return {
|
|
90
|
+
id: sessionId,
|
|
91
|
+
name,
|
|
92
|
+
machine: hostname(),
|
|
93
|
+
pid: process.pid,
|
|
94
|
+
startedAt: new Date().toISOString(),
|
|
95
|
+
cwd,
|
|
96
|
+
provider,
|
|
97
|
+
model,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mkdirSync,
|
|
3
|
+
writeFileSync,
|
|
4
|
+
readdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
unlinkSync,
|
|
7
|
+
existsSync,
|
|
8
|
+
renameSync,
|
|
9
|
+
} from 'node:fs'
|
|
10
|
+
import { join } from 'node:path'
|
|
11
|
+
import { homedir } from 'node:os'
|
|
12
|
+
import type { CrossSessionTransport } from './transport'
|
|
13
|
+
import type { AgentMessage } from '../message-bus'
|
|
14
|
+
import type { SessionInfo } from '../../shared/types'
|
|
15
|
+
|
|
16
|
+
const MIPHAM_HOME = join(homedir(), '.mipham')
|
|
17
|
+
const INBOX_DIR = join(MIPHAM_HOME, 'inbox')
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Filesystem-based cross-session transport.
|
|
21
|
+
*
|
|
22
|
+
* Messages are stored as JSON files in ~/.mipham/inbox/<session-id>/.
|
|
23
|
+
* Each message file is named: <ISO-timestamp>-<msg-id>.json
|
|
24
|
+
*/
|
|
25
|
+
export class FileInboxTransport implements CrossSessionTransport {
|
|
26
|
+
private ensureDir(dir: string): void {
|
|
27
|
+
mkdirSync(dir, { recursive: true })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async send(from: SessionInfo, toSessionId: string, msg: AgentMessage): Promise<boolean> {
|
|
31
|
+
try {
|
|
32
|
+
const inboxDir = join(INBOX_DIR, toSessionId)
|
|
33
|
+
this.ensureDir(inboxDir)
|
|
34
|
+
|
|
35
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
|
|
36
|
+
const filename = `${timestamp}-${msg.id}.json`
|
|
37
|
+
const filePath = join(inboxDir, filename)
|
|
38
|
+
|
|
39
|
+
// Atomic write: temp file then rename
|
|
40
|
+
const tmpPath = filePath + '.tmp'
|
|
41
|
+
writeFileSync(
|
|
42
|
+
tmpPath,
|
|
43
|
+
JSON.stringify({ ...msg, _fromSession: from.id, _fromMachine: from.machine }, null, 2),
|
|
44
|
+
'utf-8',
|
|
45
|
+
)
|
|
46
|
+
renameSync(tmpPath, filePath)
|
|
47
|
+
|
|
48
|
+
return true
|
|
49
|
+
} catch {
|
|
50
|
+
return false
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async poll(sessionId: string): Promise<AgentMessage[]> {
|
|
55
|
+
const inboxDir = join(INBOX_DIR, sessionId)
|
|
56
|
+
if (!existsSync(inboxDir)) return []
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const files = readdirSync(inboxDir)
|
|
60
|
+
.filter((f) => f.endsWith('.json') && !f.endsWith('.tmp'))
|
|
61
|
+
.sort() // chronological by timestamp prefix
|
|
62
|
+
|
|
63
|
+
const messages: AgentMessage[] = []
|
|
64
|
+
for (const file of files) {
|
|
65
|
+
try {
|
|
66
|
+
const raw = readFileSync(join(inboxDir, file), 'utf-8')
|
|
67
|
+
const parsed = JSON.parse(raw) as AgentMessage & {
|
|
68
|
+
_fromSession?: string
|
|
69
|
+
_fromMachine?: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Build the message body with cross-session origin prefix
|
|
73
|
+
const originPrefix = parsed._fromSession
|
|
74
|
+
? `[From: ${parsed._fromSession}@${parsed._fromMachine || 'unknown'}]\n\n`
|
|
75
|
+
: ''
|
|
76
|
+
|
|
77
|
+
messages.push({
|
|
78
|
+
id: parsed.id,
|
|
79
|
+
from: parsed.from,
|
|
80
|
+
to: parsed.to,
|
|
81
|
+
summary: parsed.summary,
|
|
82
|
+
message: `${originPrefix}${parsed.message}`,
|
|
83
|
+
timestamp: new Date(parsed.timestamp),
|
|
84
|
+
read: parsed.read,
|
|
85
|
+
type: parsed.type,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
// Mark as read by deleting the file (read-once)
|
|
89
|
+
unlinkSync(join(inboxDir, file))
|
|
90
|
+
} catch {
|
|
91
|
+
// Skip corrupt files
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return messages
|
|
96
|
+
} catch {
|
|
97
|
+
return []
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async listSessions(): Promise<SessionInfo[]> {
|
|
102
|
+
return discoverSessions()
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Import discoverSessions lazily to avoid circular dependency
|
|
107
|
+
import { discoverSessions } from './discovery'
|
|
108
|
+
|
|
109
|
+
// ── Singleton ──
|
|
110
|
+
|
|
111
|
+
let _transport: FileInboxTransport | null = null
|
|
112
|
+
|
|
113
|
+
export function getFileInboxTransport(): FileInboxTransport {
|
|
114
|
+
if (!_transport) {
|
|
115
|
+
_transport = new FileInboxTransport()
|
|
116
|
+
}
|
|
117
|
+
return _transport
|
|
118
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AgentMessage } from '../message-bus'
|
|
2
|
+
import type { SessionInfo } from '../../shared/types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Transport layer for cross-session messaging.
|
|
6
|
+
* Implementations can use filesystem, unix sockets, TCP, etc.
|
|
7
|
+
*/
|
|
8
|
+
export interface CrossSessionTransport {
|
|
9
|
+
/** Send a message to a destination session's inbox. Returns true on success. */
|
|
10
|
+
send(from: SessionInfo, toSessionId: string, msg: AgentMessage): Promise<boolean>
|
|
11
|
+
|
|
12
|
+
/** Poll for unread messages addressed to the given session. */
|
|
13
|
+
poll(sessionId: string): Promise<AgentMessage[]>
|
|
14
|
+
|
|
15
|
+
/** List all discoverable active sessions. */
|
|
16
|
+
listSessions(): Promise<SessionInfo[]>
|
|
17
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs'
|
|
2
|
+
import { join, dirname } from 'node:path'
|
|
3
|
+
|
|
4
|
+
export interface RuleEffectiveness {
|
|
5
|
+
ruleId: string
|
|
6
|
+
appliedCount: number
|
|
7
|
+
successAfterCount: number
|
|
8
|
+
preRuleFailureRate: number
|
|
9
|
+
postRuleFailureRate: number
|
|
10
|
+
status: 'active' | 'degrading' | 'disabled'
|
|
11
|
+
createdAt: string
|
|
12
|
+
lastEvaluatedAt: string
|
|
13
|
+
evaluationHistory: Array<{
|
|
14
|
+
date: string
|
|
15
|
+
appliedCount: number
|
|
16
|
+
failureRate: number
|
|
17
|
+
}>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const EVAL_THRESHOLD = 10 // evaluate after 10 applications
|
|
21
|
+
const STORE_FILE = 'effectiveness.json'
|
|
22
|
+
|
|
23
|
+
export class EffectivenessTracker {
|
|
24
|
+
private data: Map<string, RuleEffectiveness>
|
|
25
|
+
private storePath: string
|
|
26
|
+
|
|
27
|
+
constructor(storeDir: string = join(process.env.HOME || '~', '.mipham', 'rule-engine')) {
|
|
28
|
+
this.storePath = join(storeDir, STORE_FILE)
|
|
29
|
+
this.data = new Map()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
recordApplication(ruleId: string, success: boolean): void {
|
|
33
|
+
let eff = this.data.get(ruleId)
|
|
34
|
+
if (!eff) {
|
|
35
|
+
eff = {
|
|
36
|
+
ruleId,
|
|
37
|
+
appliedCount: 0,
|
|
38
|
+
successAfterCount: 0,
|
|
39
|
+
preRuleFailureRate: 1.0,
|
|
40
|
+
postRuleFailureRate: 0,
|
|
41
|
+
status: 'active',
|
|
42
|
+
createdAt: new Date().toISOString().slice(0, 10),
|
|
43
|
+
lastEvaluatedAt: '',
|
|
44
|
+
evaluationHistory: [],
|
|
45
|
+
}
|
|
46
|
+
this.data.set(ruleId, eff)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
eff.appliedCount++
|
|
50
|
+
if (success) eff.successAfterCount++
|
|
51
|
+
|
|
52
|
+
// Calculate current failure rate
|
|
53
|
+
if (eff.appliedCount >= EVAL_THRESHOLD) {
|
|
54
|
+
eff.postRuleFailureRate = 1 - eff.successAfterCount / eff.appliedCount
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Evaluate rule effectiveness and return auto-management actions.
|
|
60
|
+
*
|
|
61
|
+
* @param autoRuleManagement — when false, skip auto-degrade/disable/enable logic
|
|
62
|
+
* but still record evaluation history for manual review. Default true.
|
|
63
|
+
*/
|
|
64
|
+
evaluate(autoRuleManagement = true): {
|
|
65
|
+
upgrades: string[]
|
|
66
|
+
degradations: string[]
|
|
67
|
+
disables: string[]
|
|
68
|
+
} {
|
|
69
|
+
const result = {
|
|
70
|
+
upgrades: [] as string[],
|
|
71
|
+
degradations: [] as string[],
|
|
72
|
+
disables: [] as string[],
|
|
73
|
+
}
|
|
74
|
+
const now = new Date().toISOString().slice(0, 10)
|
|
75
|
+
|
|
76
|
+
for (const [ruleId, eff] of this.data) {
|
|
77
|
+
if (eff.appliedCount < EVAL_THRESHOLD) continue
|
|
78
|
+
|
|
79
|
+
eff.lastEvaluatedAt = now
|
|
80
|
+
eff.evaluationHistory.push({
|
|
81
|
+
date: now,
|
|
82
|
+
appliedCount: eff.appliedCount,
|
|
83
|
+
failureRate: eff.postRuleFailureRate,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// Keep max 10 history entries
|
|
87
|
+
if (eff.evaluationHistory.length > 10) {
|
|
88
|
+
eff.evaluationHistory = eff.evaluationHistory.slice(-10)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Auto-management is gated by crsi.autoRuleManagement feature flag
|
|
92
|
+
if (!autoRuleManagement) continue
|
|
93
|
+
|
|
94
|
+
// Degrade: active rule with high failure rate
|
|
95
|
+
if (eff.status === 'active' && eff.postRuleFailureRate > 0.6) {
|
|
96
|
+
eff.status = 'degrading'
|
|
97
|
+
result.degradations.push(ruleId)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Disable: degrading rule with no improvement across evaluations
|
|
101
|
+
if (eff.status === 'degrading' && eff.evaluationHistory.length >= 2) {
|
|
102
|
+
const last = eff.evaluationHistory[eff.evaluationHistory.length - 1]
|
|
103
|
+
const prev = eff.evaluationHistory[eff.evaluationHistory.length - 2]
|
|
104
|
+
if (last!.failureRate >= prev!.failureRate) {
|
|
105
|
+
eff.status = 'disabled'
|
|
106
|
+
result.disables.push(ruleId)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Upgrade: degrading rule that has improved significantly
|
|
111
|
+
if (eff.status === 'degrading' && eff.postRuleFailureRate < 0.4) {
|
|
112
|
+
eff.status = 'active'
|
|
113
|
+
result.upgrades.push(ruleId)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return result
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
getEffectiveness(ruleId: string): RuleEffectiveness | null {
|
|
121
|
+
return this.data.get(ruleId) || null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
persist(): void {
|
|
125
|
+
const dir = dirname(this.storePath)
|
|
126
|
+
mkdirSync(dir, { recursive: true })
|
|
127
|
+
|
|
128
|
+
const obj: Record<string, RuleEffectiveness> = {}
|
|
129
|
+
for (const [k, v] of this.data) {
|
|
130
|
+
obj[k] = v
|
|
131
|
+
}
|
|
132
|
+
writeFileSync(this.storePath, JSON.stringify(obj, null, 2), 'utf-8')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
load(): void {
|
|
136
|
+
if (!existsSync(this.storePath)) return
|
|
137
|
+
try {
|
|
138
|
+
const raw = JSON.parse(readFileSync(this.storePath, 'utf-8'))
|
|
139
|
+
this.data = new Map(Object.entries(raw))
|
|
140
|
+
} catch {
|
|
141
|
+
// Corrupt file — start fresh
|
|
142
|
+
this.data = new Map()
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
get allRules(): RuleEffectiveness[] {
|
|
147
|
+
return Array.from(this.data.values())
|
|
148
|
+
}
|
|
149
|
+
}
|