@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,67 @@
1
+ import type { ToolDefinition } from '../../shared/index.ts'
2
+ import { discoverSessions } from '../../agent/cross-session/discovery'
3
+
4
+ export const listAgentsTool: ToolDefinition = {
5
+ name: 'ListAgents',
6
+ description:
7
+ 'Discover active Mipham Code sessions running on this machine. ' +
8
+ 'Returns session ID, name, machine, working directory, provider, and model for each session. ' +
9
+ 'Use with SendMessage to communicate across sessions.',
10
+ category: 'agent',
11
+ permission: 'auto',
12
+ parameters: {
13
+ type: 'object',
14
+ properties: {
15
+ scope: {
16
+ type: 'string',
17
+ enum: ['local', 'all'],
18
+ default: 'local',
19
+ description:
20
+ 'Discovery scope. "local" scans this machine only. "all" is reserved for future network discovery.',
21
+ },
22
+ },
23
+ },
24
+ async execute(params) {
25
+ const scope = (params.scope as string) || 'local'
26
+
27
+ if (scope === 'all') {
28
+ return {
29
+ success: true,
30
+ content:
31
+ 'Network discovery is not yet available. Showing local sessions only.\n\n' +
32
+ formatSessionList(discoverSessions()),
33
+ }
34
+ }
35
+
36
+ const sessions = discoverSessions()
37
+
38
+ if (sessions.length === 0) {
39
+ return {
40
+ success: true,
41
+ content: 'No active Mipham Code sessions found on this machine.',
42
+ }
43
+ }
44
+
45
+ return {
46
+ success: true,
47
+ content: formatSessionList(sessions),
48
+ }
49
+ },
50
+ }
51
+
52
+ function formatSessionList(sessions: import('../../shared/types').SessionInfo[]): string {
53
+ const lines: string[] = [`${sessions.length} active session(s):\n`]
54
+
55
+ for (const s of sessions) {
56
+ const modelInfo = s.model ? ` · ${s.model}` : ''
57
+ const providerInfo = s.provider ? ` (${s.provider})` : ''
58
+ lines.push(` ${s.id}`)
59
+ lines.push(` Name: ${s.name}`)
60
+ lines.push(` Machine: ${s.machine}${modelInfo}${providerInfo}`)
61
+ lines.push(` PID: ${s.pid} · Started: ${s.startedAt}`)
62
+ if (s.cwd) lines.push(` CWD: ${s.cwd}`)
63
+ lines.push('')
64
+ }
65
+
66
+ return lines.join('\n').trim()
67
+ }
@@ -1,13 +1,12 @@
1
1
  import type { ToolDefinition } from '../../shared/index.ts'
2
- import { getMessageBus } from '../../agent/message-bus'
2
+ import { getMessageRouter } from '../../agent/message-router'
3
3
 
4
4
  export const sendMessageTool: ToolDefinition = {
5
5
  name: 'SendMessage',
6
6
  description:
7
- 'Send a message to another agent or the main conversation. ' +
8
- 'Use "main" as the recipient to message the parent session, ' +
9
- 'or a background task ID (e.g., "bg-1-xxx") to message a specific agent. ' +
10
- 'Messages are stored in the AgentMessageBus and can be polled by the recipient.',
7
+ 'Send a message to another agent or session. ' +
8
+ 'Use "main" for the parent conversation, a background task ID for same-process agents, ' +
9
+ 'or a session ID for cross-session messaging (use ListAgents to discover sessions).',
11
10
  category: 'agent',
12
11
  permission: 'auto',
13
12
  parameters: {
@@ -16,7 +15,7 @@ export const sendMessageTool: ToolDefinition = {
16
15
  to: {
17
16
  type: 'string',
18
17
  description:
19
- 'Recipient: "main" for the parent conversation, a background task ID, or an agent name.',
18
+ 'Recipient: "main" for the parent conversation, a background task ID, or a session ID for cross-session messaging.',
20
19
  },
21
20
  summary: {
22
21
  type: 'string',
@@ -34,34 +33,32 @@ export const sendMessageTool: ToolDefinition = {
34
33
  const summary = (params.summary as string) || '(no subject)'
35
34
  const message = params.message as string
36
35
 
37
- // Determine sender: use sessionId or 'main'
38
36
  const from =
39
37
  ctx.sessionId === 'sub-agent'
40
38
  ? `sub-agent-${Date.now().toString(36)}`
41
39
  : ctx.sessionId || 'main'
42
40
 
43
- try {
44
- const bus = getMessageBus()
45
- const msgId = bus.post(from, to, summary, message)
41
+ const router = getMessageRouter()
42
+ const result = await router.route(from, to, summary, message)
46
43
 
47
- const unreadForRecipient = bus.unreadCount(to)
48
-
49
- return {
50
- success: true,
51
- content:
52
- `── Message Sent ──\n\n` +
53
- `ID: ${msgId}\n` +
54
- `From: ${from}\n` +
55
- `To: ${to}\n` +
56
- `Summary: ${summary.slice(0, 100)}\n\n` +
57
- `The recipient has ${unreadForRecipient} unread message(s).`,
58
- }
59
- } catch (err) {
44
+ if (!result.success) {
60
45
  return {
61
46
  success: false,
62
47
  content: '',
63
- error: `Failed to send message: ${String(err)}`,
48
+ error: `Failed to send message: ${result.error}`,
64
49
  }
65
50
  }
51
+
52
+ const routedLabel = result.routedTo === 'bus' ? 'in-process' : 'cross-session'
53
+
54
+ return {
55
+ success: true,
56
+ content:
57
+ `── Message Sent (${routedLabel}) ──\n\n` +
58
+ `ID: ${result.messageId}\n` +
59
+ `From: ${from}\n` +
60
+ `To: ${to}\n` +
61
+ `Summary: ${summary.slice(0, 100)}`,
62
+ }
66
63
  },
67
64
  }
@@ -122,6 +122,57 @@ function isBlocked(command: string): string | null {
122
122
  return null // safe
123
123
  }
124
124
 
125
+ /**
126
+ * Detect sandbox violations from stderr output.
127
+ * Parses common OS-level error patterns indicating denied access.
128
+ */
129
+ export function detectViolations(stderr: string): string[] {
130
+ const violations: string[] = []
131
+
132
+ // File access violations
133
+ const accessPatterns = /(?:Permission denied|EACCES|EPERM|Operation not permitted)/gi
134
+ const accessMatches = stderr.match(accessPatterns)
135
+ if (accessMatches && accessMatches.length > 0) {
136
+ // Extract file paths from error messages
137
+ const pathPattern =
138
+ /(?:Permission denied|EACCES|EPERM|Operation not permitted).*?['"]?(\/[^\s'"]+)['"]?/gi
139
+ const paths: string[] = []
140
+ let match: RegExpExecArray | null
141
+ while ((match = pathPattern.exec(stderr)) !== null) {
142
+ paths.push(match[1]!)
143
+ }
144
+
145
+ if (paths.length > 0) {
146
+ violations.push(` File access denied: ${paths.join(', ')}`)
147
+ } else {
148
+ violations.push(` File access denied (${accessMatches.length} occurrence(s))`)
149
+ }
150
+ }
151
+
152
+ // Network access violations
153
+ const netPatterns =
154
+ /(?:Network is unreachable|Connection refused|ECONNREFUSED|ENETUNREACH|Could not resolve host|Name or service not known|ETIMEDOUT|Connection timed out)/gi
155
+ const netMatches = stderr.match(netPatterns)
156
+ if (netMatches && netMatches.length > 0) {
157
+ // Extract host:port from error messages
158
+ const hostPattern =
159
+ /(?:connect to|Could not resolve host|Failed to connect to)\s+([^\s:]+(?::\d+)?)/gi
160
+ const hosts: string[] = []
161
+ let match: RegExpExecArray | null
162
+ while ((match = hostPattern.exec(stderr)) !== null) {
163
+ hosts.push(match[1]!)
164
+ }
165
+
166
+ if (hosts.length > 0) {
167
+ violations.push(` Network access denied: ${hosts.join(', ')}`)
168
+ } else {
169
+ violations.push(` Network access denied (${netMatches.length} occurrence(s))`)
170
+ }
171
+ }
172
+
173
+ return violations
174
+ }
175
+
125
176
  export const bashTool: ToolDefinition = {
126
177
  name: 'Bash',
127
178
  description:
@@ -173,6 +224,9 @@ export const bashTool: ToolDefinition = {
173
224
  const exitCode = await proc.exited
174
225
  clearTimeout(timer)
175
226
 
227
+ // Read stderr for violation detection and error reporting
228
+ const rawStderr = await new Response(proc.stderr).text()
229
+
176
230
  // ── Credential masking: scrub output ──
177
231
  let output = rawOutput
178
232
  if (credentialConfig?.enabled && credentialConfig.output_scrubbing.enabled) {
@@ -180,20 +234,30 @@ export const bashTool: ToolDefinition = {
180
234
  output = maskOutput(rawOutput, credentialConfig)
181
235
  }
182
236
 
237
+ // ── Sandbox violation detection ──
238
+ const violations = detectViolations(rawStderr)
239
+
183
240
  if (exitCode !== 0) {
184
- const rawStderr = await new Response(proc.stderr).text()
185
241
  const stderr =
186
242
  credentialConfig?.enabled && credentialConfig.output_scrubbing.enabled
187
243
  ? (await import('../../core/credential-masker')).maskOutput(rawStderr, credentialConfig)
188
244
  : rawStderr
245
+ let errorContent = output.slice(0, 5_000)
246
+ if (violations.length > 0) {
247
+ errorContent += '\n\n── Sandbox Violations ──\n' + violations.join('\n')
248
+ }
189
249
  return {
190
250
  success: false,
191
- content: output.slice(0, 5_000),
251
+ content: errorContent,
192
252
  error: `Exit code ${exitCode}: ${stderr.slice(0, 1_000)}`,
193
253
  }
194
254
  }
195
255
 
196
- return { success: true, content: output.slice(0, 100_000) || '(no output)' }
256
+ let successContent = output.slice(0, 100_000) || '(no output)'
257
+ if (violations.length > 0) {
258
+ successContent += '\n\n── Sandbox Violations ──\n' + violations.join('\n')
259
+ }
260
+ return { success: true, content: successContent }
197
261
  } catch (err) {
198
262
  return {
199
263
  success: false,
@@ -29,6 +29,7 @@ import { toolSearchTool } from './system/tool-search'
29
29
  import { artifactTool } from './artifact/artifact'
30
30
  import { reportFindingsTool } from './agent/report-findings'
31
31
  import { sendMessageTool } from './agent/send-message'
32
+ import { listAgentsTool } from './agent/list-agents'
32
33
  import { computerUseTool } from './computer/computer-use'
33
34
  import { scheduleWakeupTool } from './scheduling/schedule-wakeup.js'
34
35
  import { cronCreateTool, cronDeleteTool, cronListTool } from './scheduling/cron.js'
@@ -145,6 +146,7 @@ export function createToolRegistry(): Map<string, ToolDefinition> {
145
146
  withValidation(workflowTool),
146
147
  withValidation(reportFindingsTool),
147
148
  withValidation(sendMessageTool),
149
+ withValidation(listAgentsTool),
148
150
  // Network tools
149
151
  withValidation(webFetchTool),
150
152
  withValidation(webSearchTool),
@@ -446,6 +446,124 @@ const skillsCmd: CommandHandler = (ctx) => {
446
446
  return { content: lines.join('\n') }
447
447
  }
448
448
 
449
+ // ═══════════════════════════════════════════════════════════════
450
+ // CRSI (Conversational Rule Self-Improvement)
451
+ // ═══════════════════════════════════════════════════════════════
452
+
453
+ const crsiRulesCmd: CommandHandler = (ctx) => {
454
+ const engine = ctx.engine.getRuleEngine()
455
+ if (!engine) {
456
+ return { content: 'CRSI rule engine is not available.' }
457
+ }
458
+ const rules = engine.getActiveRules()
459
+ if (rules.length === 0) {
460
+ return { content: 'No active CRSI rules.' }
461
+ }
462
+ const lines: string[] = ['## Active CRSI Rules', '']
463
+ for (const r of rules) {
464
+ const status = r.enabled ? '✅' : '⛔'
465
+ lines.push(`- \`${r.id}\` [${r.category}] ${r.toolName} — ${r.source} ${status}`)
466
+ }
467
+ lines.push('', `Total: ${rules.length} active rules`)
468
+ lines.push('', 'Use `/crsi disable <rule-id>` to disable a rule.')
469
+ return { content: lines.join('\n') }
470
+ }
471
+
472
+ const crsiDisableCmd: CommandHandler = (ctx, args) => {
473
+ const engine = ctx.engine.getRuleEngine()
474
+ if (!engine) {
475
+ return { content: 'CRSI rule engine is not available.' }
476
+ }
477
+ const ruleId = args[0]?.trim()
478
+ if (!ruleId) {
479
+ return { content: 'Usage: /crsi disable <rule-id>' }
480
+ }
481
+ engine.setRuleEnabled(ruleId, false)
482
+ return {
483
+ content: `Rule \`${ruleId}\` has been disabled. Use \`/crsi restore ${ruleId}\` to re-enable.`,
484
+ }
485
+ }
486
+
487
+ const crsiAnalyzeCmd: CommandHandler = async (ctx) => {
488
+ const analyzer = ctx.engine.getPatternAnalyzer()
489
+ const engine = ctx.engine.getRuleEngine()
490
+ if (!engine) {
491
+ return { content: 'CRSI system is not available.' }
492
+ }
493
+
494
+ const patterns = analyzer.analyzeAllAgents()
495
+ if (patterns.length === 0) {
496
+ return { content: 'No failure patterns found across agents.' }
497
+ }
498
+
499
+ let registered = 0
500
+ for (const pattern of patterns) {
501
+ const toolRule = analyzer.toToolRule(pattern)
502
+ engine.register(toolRule)
503
+ registered++
504
+ }
505
+
506
+ const lines: string[] = [
507
+ '## CRSI Analysis Complete',
508
+ '',
509
+ `Found ${patterns.length} patterns, ${registered} rules registered.`,
510
+ '',
511
+ ]
512
+ for (const p of patterns) {
513
+ lines.push(
514
+ `- [${p.category}] \`${p.agentName}\` — ${p.frequency} failures (${p.confidence} confidence)`,
515
+ )
516
+ }
517
+ return { content: lines.join('\n') }
518
+ }
519
+
520
+ const crsiRestoreCmd: CommandHandler = (ctx, args) => {
521
+ const engine = ctx.engine.getRuleEngine()
522
+ if (!engine) {
523
+ return { content: 'CRSI rule engine is not available.' }
524
+ }
525
+ const ruleId = args.join(' ').trim()
526
+ if (!ruleId) {
527
+ return { content: 'Usage: /crsi restore <rule-id>' }
528
+ }
529
+ engine.setRuleEnabled(ruleId, true)
530
+ return { content: `Rule \`${ruleId}\` has been re-enabled.` }
531
+ }
532
+
533
+ const crsiStatsCmd: CommandHandler = async (ctx) => {
534
+ const engine = ctx.engine.getRuleEngine()
535
+ const tracker = ctx.engine.getEffectivenessTracker()
536
+ if (!engine) {
537
+ return { content: 'CRSI rule engine is not available.' }
538
+ }
539
+
540
+ const rules = engine.getActiveRules()
541
+ const lines: string[] = ['## CRSI Statistics', '']
542
+ lines.push(`Total active rules: ${rules.length}`)
543
+ lines.push(`Builtin: ${rules.filter((r) => r.source === 'builtin').length}`)
544
+ lines.push(`Auto-generated: ${rules.filter((r) => r.source === 'pattern-analyzer').length}`)
545
+ lines.push(`Manual: ${rules.filter((r) => r.source === 'manual').length}`)
546
+
547
+ if (tracker) {
548
+ let totalInterceptions = 0
549
+ let totalSuccesses = 0
550
+ for (const r of rules) {
551
+ const eff = tracker.getEffectiveness(r.id)
552
+ if (eff) {
553
+ totalInterceptions += eff.appliedCount
554
+ totalSuccesses += eff.successAfterCount
555
+ }
556
+ }
557
+ lines.push('')
558
+ lines.push(`Total interceptions: ${totalInterceptions}`)
559
+ lines.push(
560
+ `Success rate after rules: ${totalInterceptions > 0 ? Math.round((totalSuccesses / totalInterceptions) * 100) : 0}%`,
561
+ )
562
+ }
563
+
564
+ return { content: lines.join('\n') }
565
+ }
566
+
449
567
  // ═══════════════════════════════════════════════════════════════
450
568
  // Workflow
451
569
  // ═══════════════════════════════════════════════════════════════
@@ -3072,6 +3190,11 @@ const commandsListCmd: CommandHandler = () => {
3072
3190
  '/plugin-enable': 'Plugins',
3073
3191
  '/plugin-disable': 'Plugins',
3074
3192
  '/commands': 'Tools & Skills',
3193
+ '/crsi rules': 'Tools & Skills',
3194
+ '/crsi disable': 'Tools & Skills',
3195
+ '/crsi analyze': 'Tools & Skills',
3196
+ '/crsi restore': 'Tools & Skills',
3197
+ '/crsi stats': 'Tools & Skills',
3075
3198
  '/plan': 'Workflow',
3076
3199
  '/no-plan': 'Workflow',
3077
3200
  '/tdd': 'Workflow',
@@ -3209,6 +3332,11 @@ registry.set('/remove-plugin', removePluginCmd)
3209
3332
  registry.set('/plugin-enable', pluginEnableCmd)
3210
3333
  registry.set('/plugin-disable', pluginDisableCmd)
3211
3334
  registry.set('/commands', commandsListCmd)
3335
+ registry.set('/crsi rules', crsiRulesCmd)
3336
+ registry.set('/crsi disable', crsiDisableCmd)
3337
+ registry.set('/crsi analyze', crsiAnalyzeCmd)
3338
+ registry.set('/crsi restore', crsiRestoreCmd)
3339
+ registry.set('/crsi stats', crsiStatsCmd)
3212
3340
 
3213
3341
  // Workflow
3214
3342
  registry.set('/plan', planCmd)
@@ -3359,6 +3487,11 @@ const COMMAND_DESCRIPTIONS: Record<string, string> = {
3359
3487
  '/remove-plugin': 'Remove an installed plugin',
3360
3488
  '/plugin-enable': 'Enable a disabled plugin',
3361
3489
  '/plugin-disable': 'Disable an enabled plugin',
3490
+ '/crsi rules': 'List all active CRSI rules with their status',
3491
+ '/crsi disable': 'Disable a CRSI rule by ID',
3492
+ '/crsi analyze': 'Manually trigger CRSI pattern analysis across all agents',
3493
+ '/crsi restore': 'Restore a disabled or degraded CRSI rule',
3494
+ '/crsi stats': 'Show CRSI overall effectiveness statistics',
3362
3495
  '/plan': 'Enter plan mode',
3363
3496
  '/no-plan': 'Exit plan mode',
3364
3497
  '/tdd': 'Test-Driven Development workflow (RED → GREEN → REFACTOR)',