@miphamai/cli 0.12.0 → 0.14.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/package.json +1 -1
- package/skills/workflows/audit.js +51 -0
- package/skills/workflows/hunt.js +95 -0
- package/skills/workflows/judge.js +55 -0
- package/skills/workflows/migrate.js +92 -0
- package/skills/workflows/research.js +87 -0
- package/skills/workflows/review.js +92 -0
- package/src/agent/background-registry.ts +11 -0
- package/src/agent-view/agent-view-manager.ts +10 -0
- package/src/config/defaults.ts +26 -1
- package/src/config/loader.ts +79 -1
- package/src/core/credential-masker.ts +203 -0
- package/src/core/instructions.ts +28 -0
- package/src/core/permission.ts +62 -8
- package/src/index.tsx +8 -1
- package/src/plugin/plugin-manager.ts +39 -2
- package/src/shared/types.ts +43 -0
- package/src/tools/agent/workflow.ts +67 -3
- package/src/tools/exec/bash.ts +29 -3
- package/src/tools/file/read.ts +30 -1
- package/src/ui/app.tsx +6 -1
- package/src/ui/chat.tsx +50 -17
- package/src/ui/commands.ts +451 -0
- package/src/workflow/primitives/loop.ts +103 -0
- package/src/workflow/primitives/verify.ts +275 -0
- package/src/workflow/runtime.ts +17 -1
package/src/tools/file/read.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { readFileSync, existsSync, statSync } from 'node:fs'
|
|
2
|
-
import type { ToolDefinition } from '../../shared/index.ts'
|
|
2
|
+
import type { ToolDefinition, CredentialMaskingConfig } from '../../shared/index.ts'
|
|
3
3
|
import { resolveSafe } from '../../security/path'
|
|
4
4
|
|
|
5
|
+
// Injected at startup — set via index.tsx
|
|
6
|
+
let credentialConfig: CredentialMaskingConfig | undefined
|
|
7
|
+
|
|
8
|
+
export function setCredentialMaskingConfigForRead(config: CredentialMaskingConfig): void {
|
|
9
|
+
credentialConfig = config
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
export const readTool: ToolDefinition = {
|
|
6
13
|
name: 'Read',
|
|
7
14
|
description: 'Read a file from the local filesystem. Supports offset and limit for large files.',
|
|
@@ -37,6 +44,28 @@ export const readTool: ToolDefinition = {
|
|
|
37
44
|
const offset = (params.offset as number) || 0
|
|
38
45
|
const limit = (params.limit as number) || 2000
|
|
39
46
|
const content = readFileSync(filePath, 'utf-8')
|
|
47
|
+
|
|
48
|
+
// ── Credential masking ──
|
|
49
|
+
if (credentialConfig) {
|
|
50
|
+
const { matchCredentialFile, maskContent, CREDENTIAL_SENTINEL } =
|
|
51
|
+
await import('../../core/credential-masker')
|
|
52
|
+
const rule = matchCredentialFile(filePath, credentialConfig)
|
|
53
|
+
if (rule) {
|
|
54
|
+
const masked = maskContent(content, rule)
|
|
55
|
+
// Full-file mask: return sentinel immediately (offset/limit don't apply)
|
|
56
|
+
if (masked === CREDENTIAL_SENTINEL) {
|
|
57
|
+
return { success: true, content: masked }
|
|
58
|
+
}
|
|
59
|
+
// Extract mode: apply offset/limit on masked content
|
|
60
|
+
const maskedLines = masked.split('\n')
|
|
61
|
+
const maskedSlice = maskedLines.slice(offset, offset + limit)
|
|
62
|
+
const maskedResult = maskedSlice
|
|
63
|
+
.map((l, i) => `${String(offset + i + 1).padStart(6, ' ')}\t${l}`)
|
|
64
|
+
.join('\n')
|
|
65
|
+
return { success: true, content: maskedResult }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
40
69
|
const lines = content.split('\n')
|
|
41
70
|
const slice = lines.slice(offset, offset + limit)
|
|
42
71
|
const result = slice
|
package/src/ui/app.tsx
CHANGED
|
@@ -372,6 +372,11 @@ export function App({
|
|
|
372
372
|
setPickerOpen((prev) => !prev)
|
|
373
373
|
return
|
|
374
374
|
}
|
|
375
|
+
// Ctrl+F → toggle focus mode
|
|
376
|
+
if (_input === '\x06') {
|
|
377
|
+
setFocusMode((prev) => !prev)
|
|
378
|
+
return
|
|
379
|
+
}
|
|
375
380
|
// Ctrl+O → toggle last tool call expand/collapse
|
|
376
381
|
if (_input === '\x0f') {
|
|
377
382
|
setMessages((prev) => {
|
|
@@ -480,7 +485,7 @@ export function App({
|
|
|
480
485
|
● {PERMISSION_LABELS[permissionMode]}
|
|
481
486
|
</Text>
|
|
482
487
|
<Text dimColor> · Shift+Tab mode</Text>
|
|
483
|
-
<Text dimColor> · Ctrl+P model · Ctrl+O expand</Text>
|
|
488
|
+
<Text dimColor> · Ctrl+P model · Ctrl+F focus · Ctrl+O expand</Text>
|
|
484
489
|
<Text dimColor> · Esc cancel · /help · ← agents</Text>
|
|
485
490
|
</Box>
|
|
486
491
|
</Box>
|
package/src/ui/chat.tsx
CHANGED
|
@@ -18,16 +18,16 @@ function displayCwd(): string {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export function ChatPanel({ messages, focusMode }: ChatPanelProps) {
|
|
21
|
-
// In focus mode,
|
|
22
|
-
const displayMessages = focusMode ?
|
|
21
|
+
// In focus mode, compact tool activity into summary lines
|
|
22
|
+
const displayMessages = focusMode ? compactForFocus(messages) : messages
|
|
23
23
|
|
|
24
24
|
return (
|
|
25
25
|
<Box flexDirection="column" marginY={1} flexGrow={1}>
|
|
26
26
|
{focusMode && messages.length > 0 && (
|
|
27
27
|
<Box marginBottom={1}>
|
|
28
28
|
<Text dimColor>
|
|
29
|
-
🔍 Focus
|
|
30
|
-
|
|
29
|
+
🔍 Focus — {countHidden(messages, displayMessages)} hidden · /focus to toggle · Ctrl+O
|
|
30
|
+
to expand
|
|
31
31
|
</Text>
|
|
32
32
|
</Box>
|
|
33
33
|
)}
|
|
@@ -92,22 +92,55 @@ export function ChatPanel({ messages, focusMode }: ChatPanelProps) {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
95
|
+
* Compact messages for focus view:
|
|
96
|
+
* - Groups consecutive tool calls into [▶ N tools] ↳ name1 ↳ name2 summary lines
|
|
97
|
+
* - Keeps all user, assistant, and system messages intact
|
|
98
98
|
*/
|
|
99
|
-
function
|
|
100
|
-
|
|
99
|
+
function compactForFocus(messages: ChatMessage[]): ChatMessage[] {
|
|
100
|
+
const result: ChatMessage[] = []
|
|
101
|
+
let toolGroup: ChatMessage[] = []
|
|
101
102
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
103
|
+
for (const msg of messages) {
|
|
104
|
+
if (msg.toolMeta) {
|
|
105
|
+
toolGroup.push(msg)
|
|
106
|
+
} else {
|
|
107
|
+
// Flush pending tool group
|
|
108
|
+
if (toolGroup.length > 0) {
|
|
109
|
+
result.push(summarizeToolGroup(toolGroup))
|
|
110
|
+
toolGroup = []
|
|
111
|
+
}
|
|
112
|
+
result.push(msg)
|
|
108
113
|
}
|
|
109
114
|
}
|
|
115
|
+
// Flush remaining
|
|
116
|
+
if (toolGroup.length > 0) {
|
|
117
|
+
result.push(summarizeToolGroup(toolGroup))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return result
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Summarize a group of tool calls into a single compact message. */
|
|
124
|
+
function summarizeToolGroup(tools: ChatMessage[]): ChatMessage {
|
|
125
|
+
const names = tools.map((t) => t.toolMeta!.name)
|
|
126
|
+
const uniqueNames = [...new Set(names)]
|
|
127
|
+
const toolList = uniqueNames.map((n) => `↳ ${n}`).join(' ')
|
|
128
|
+
return {
|
|
129
|
+
role: 'system',
|
|
130
|
+
content: `[▶ ${tools.length} tool${tools.length > 1 ? 's' : ''}] ${toolList}`,
|
|
131
|
+
toolMeta: {
|
|
132
|
+
name: `tools (${tools.length})`,
|
|
133
|
+
input: '',
|
|
134
|
+
output: tools.map((t) => t.content).join('\n'),
|
|
135
|
+
collapsed: true,
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
}
|
|
110
139
|
|
|
111
|
-
|
|
112
|
-
|
|
140
|
+
/** Count how many messages are hidden in focus mode. */
|
|
141
|
+
function countHidden(all: ChatMessage[], compacted: ChatMessage[]): string {
|
|
142
|
+
const toolCount = all.filter((m) => m.toolMeta).length
|
|
143
|
+
const hidden = all.length - compacted.length
|
|
144
|
+
if (hidden <= 0) return `showing all ${all.length} messages`
|
|
145
|
+
return `${hidden} tool outputs folded (${toolCount} total tool calls)`
|
|
113
146
|
}
|
package/src/ui/commands.ts
CHANGED
|
@@ -2388,6 +2388,111 @@ const workflowsCmd: CommandHandler = async () => {
|
|
|
2388
2388
|
return { content: lines.join('\n') }
|
|
2389
2389
|
}
|
|
2390
2390
|
|
|
2391
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2392
|
+
// /workflow <task> — auto-generate + execute
|
|
2393
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2394
|
+
|
|
2395
|
+
const workflowSaveCmd = async (name: string): Promise<CommandResult> => {
|
|
2396
|
+
const { existsSync, mkdirSync, writeFileSync, readFileSync } = await import('node:fs')
|
|
2397
|
+
const { join } = await import('node:path')
|
|
2398
|
+
|
|
2399
|
+
const targetDir = join(process.cwd(), '.claude', 'workflows')
|
|
2400
|
+
if (!existsSync(targetDir)) {
|
|
2401
|
+
mkdirSync(targetDir, { recursive: true })
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
// Read the last-run state persisted by the Workflow tool
|
|
2405
|
+
const stateFile = join(targetDir, '.last-run.json')
|
|
2406
|
+
if (!existsSync(stateFile)) {
|
|
2407
|
+
return { content: 'No recent workflow run found. Run a workflow first with /workflow <task>.' }
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
try {
|
|
2411
|
+
const state = JSON.parse(readFileSync(stateFile, 'utf-8'))
|
|
2412
|
+
const script = state.script as string
|
|
2413
|
+
if (!script) {
|
|
2414
|
+
return { content: 'No script found in last run state.' }
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
const safeName = name.replace(/[^a-zA-Z0-9_-]/g, '-')
|
|
2418
|
+
const scriptPath = join(targetDir, `${safeName}.js`)
|
|
2419
|
+
writeFileSync(scriptPath, script, 'utf-8')
|
|
2420
|
+
|
|
2421
|
+
return {
|
|
2422
|
+
content: `Workflow saved to ${scriptPath}\nUse /workflow run ${safeName} to run it again.`,
|
|
2423
|
+
}
|
|
2424
|
+
} catch (err) {
|
|
2425
|
+
return { content: `Failed to save workflow: ${String(err)}` }
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
const workflowRunCmd = async (name: string): Promise<CommandResult> => {
|
|
2430
|
+
const { existsSync } = await import('node:fs')
|
|
2431
|
+
const { join } = await import('node:path')
|
|
2432
|
+
|
|
2433
|
+
const safeName = name.replace(/[^a-zA-Z0-9_-]/g, '-')
|
|
2434
|
+
|
|
2435
|
+
const locations = [
|
|
2436
|
+
join(process.cwd(), '.claude', 'workflows'),
|
|
2437
|
+
join(process.env.HOME || '~', '.claude', 'workflows'),
|
|
2438
|
+
]
|
|
2439
|
+
|
|
2440
|
+
for (const loc of locations) {
|
|
2441
|
+
const scriptPath = join(loc, `${safeName}.js`)
|
|
2442
|
+
if (existsSync(scriptPath)) {
|
|
2443
|
+
return {
|
|
2444
|
+
content: '',
|
|
2445
|
+
forwardToAI:
|
|
2446
|
+
`Read the workflow script at ${scriptPath}, then call the Workflow tool with ` +
|
|
2447
|
+
`the file contents as the "script" parameter to execute it. Report the results.`,
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
return {
|
|
2453
|
+
content: `Workflow "${safeName}" not found in .claude/workflows/ or ~/.claude/workflows/`,
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
const workflowAutoCmd: CommandHandler = async (_ctx, args) => {
|
|
2458
|
+
const task = args.join(' ').trim()
|
|
2459
|
+
|
|
2460
|
+
if (!task) {
|
|
2461
|
+
return {
|
|
2462
|
+
content:
|
|
2463
|
+
'Usage: /workflow <task description>\n\n' +
|
|
2464
|
+
'Describes the task, and the AI will generate a workflow script to execute it.\n' +
|
|
2465
|
+
'Examples:\n' +
|
|
2466
|
+
' /workflow audit all routes for missing auth\n' +
|
|
2467
|
+
' /workflow research the impact of React 19 on our codebase\n' +
|
|
2468
|
+
' /workflow find all hardcoded credentials in the codebase\n\n' +
|
|
2469
|
+
'Sub-commands:\n' +
|
|
2470
|
+
' /workflow save <name> — save last successful workflow script\n' +
|
|
2471
|
+
' /workflow run <name> — run a saved workflow by name\n' +
|
|
2472
|
+
' /workflows — list all saved workflow scripts',
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
// Sub-commands
|
|
2477
|
+
const firstArg = args[0] || ''
|
|
2478
|
+
if (firstArg === 'save' && args[1]) {
|
|
2479
|
+
return workflowSaveCmd(args.slice(1).join(' '))
|
|
2480
|
+
}
|
|
2481
|
+
if (firstArg === 'run' && args[1]) {
|
|
2482
|
+
return workflowRunCmd(args.slice(1).join(' '))
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
// Default: forward to AI to generate + execute workflow
|
|
2486
|
+
return {
|
|
2487
|
+
content: '',
|
|
2488
|
+
forwardToAI:
|
|
2489
|
+
`Write and execute a workflow script for this task: ${task}\n\n` +
|
|
2490
|
+
`Use the Workflow tool to execute the generated script. ` +
|
|
2491
|
+
`After the workflow completes, summarize the results and offer to save the script ` +
|
|
2492
|
+
`with /workflow save <name> if it is reusable.`,
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2391
2496
|
// ═══════════════════════════════════════════════════════════════
|
|
2392
2497
|
// Permissions
|
|
2393
2498
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -3052,6 +3157,223 @@ Permission level is controlled by /config permission <level>.`,
|
|
|
3052
3157
|
// Security / Audit — security review checklist
|
|
3053
3158
|
// ═══════════════════════════════════════════════════════════════
|
|
3054
3159
|
|
|
3160
|
+
const promptAuditCmd: CommandHandler = async () => {
|
|
3161
|
+
const { existsSync, readFileSync, readdirSync, statSync } = await import('node:fs')
|
|
3162
|
+
const { join, extname, basename } = await import('node:path')
|
|
3163
|
+
const cwd = process.cwd()
|
|
3164
|
+
|
|
3165
|
+
// Patterns that suggest prompts written for older/less-capable models
|
|
3166
|
+
const AUDIT_RULES: Array<{
|
|
3167
|
+
id: string
|
|
3168
|
+
pattern: RegExp
|
|
3169
|
+
severity: 'low' | 'medium' | 'high'
|
|
3170
|
+
message: string
|
|
3171
|
+
suggestion: string
|
|
3172
|
+
}> = [
|
|
3173
|
+
{
|
|
3174
|
+
id: 'over-explaining',
|
|
3175
|
+
pattern:
|
|
3176
|
+
/you are (a|an) (helpful |friendly |knowledgeable )?(AI |language model |assistant)/i,
|
|
3177
|
+
severity: 'low',
|
|
3178
|
+
message: 'Self-identification preamble ("You are a...")',
|
|
3179
|
+
suggestion: "Modern models don't need role preamble. Remove or shorten to 1 line.",
|
|
3180
|
+
},
|
|
3181
|
+
{
|
|
3182
|
+
id: 'step-by-step',
|
|
3183
|
+
pattern: /(think|reason|work) (step[ -]?by[ -]?step|through this|carefully about this)/i,
|
|
3184
|
+
severity: 'low',
|
|
3185
|
+
message: 'Step-by-step reasoning prompt',
|
|
3186
|
+
suggestion: 'Reasoning models handle this natively. Remove for token savings.',
|
|
3187
|
+
},
|
|
3188
|
+
{
|
|
3189
|
+
id: 'do-not-list',
|
|
3190
|
+
pattern:
|
|
3191
|
+
/(do not|never|you must not|don't|under no circumstances).*\n.*(do not|never|you must not|don't)/i,
|
|
3192
|
+
severity: 'medium',
|
|
3193
|
+
message: 'Multiple "do not" constraints',
|
|
3194
|
+
suggestion: 'Modern models need fewer prohibitions. Consolidate to 1-2 key constraints.',
|
|
3195
|
+
},
|
|
3196
|
+
{
|
|
3197
|
+
id: 'token-waste',
|
|
3198
|
+
pattern:
|
|
3199
|
+
/(remember|keep in mind|it is important to note|please note that|i want to emphasize)/i,
|
|
3200
|
+
severity: 'low',
|
|
3201
|
+
message: 'Filler emphasis phrases ("remember", "please note")',
|
|
3202
|
+
suggestion: 'Remove filler. State the instruction directly.',
|
|
3203
|
+
},
|
|
3204
|
+
{
|
|
3205
|
+
id: 'format-over-spec',
|
|
3206
|
+
pattern: /(you must (always |only )?respond (in|with|using) (JSON|XML|YAML|markdown|HTML))/i,
|
|
3207
|
+
severity: 'low',
|
|
3208
|
+
message: 'Over-specified output format',
|
|
3209
|
+
suggestion: 'Modern models follow format instructions with fewer words. Shorten.',
|
|
3210
|
+
},
|
|
3211
|
+
{
|
|
3212
|
+
id: 'example-overload',
|
|
3213
|
+
pattern: /((?:example|for instance|e\.g\.,).*\n){3,}/i,
|
|
3214
|
+
severity: 'low',
|
|
3215
|
+
message: 'Three or more examples in sequence',
|
|
3216
|
+
suggestion: '1-2 examples suffice for modern models. Remove redundant ones.',
|
|
3217
|
+
},
|
|
3218
|
+
{
|
|
3219
|
+
id: 'old-model-ref',
|
|
3220
|
+
pattern: /(GPT-3|GPT-3\.5|Claude\s*[12][^3]|text-davinci|command-r|llama\s*2)/i,
|
|
3221
|
+
severity: 'high',
|
|
3222
|
+
message: 'Reference to older model',
|
|
3223
|
+
suggestion: 'Update model references to current generation (Sonnet 5, Opus 5, GPT-5, etc.).',
|
|
3224
|
+
},
|
|
3225
|
+
{
|
|
3226
|
+
id: 'long-preamble',
|
|
3227
|
+
pattern: /^.{200,}$/m,
|
|
3228
|
+
severity: 'medium',
|
|
3229
|
+
message: 'Very long single-line instructions (200+ chars)',
|
|
3230
|
+
suggestion: 'Break into bullet points. Modern models parse structured prompts better.',
|
|
3231
|
+
},
|
|
3232
|
+
{
|
|
3233
|
+
id: 'verbose-constraint',
|
|
3234
|
+
pattern:
|
|
3235
|
+
/you (must|should|shall) (always |only |never )?(ensure|guarantee|verify|validate|confirm|cross-check|double-check)/i,
|
|
3236
|
+
severity: 'medium',
|
|
3237
|
+
message: 'Overly verbose constraint language',
|
|
3238
|
+
suggestion: 'Replace "you must ensure that" with imperative: "Ensure".',
|
|
3239
|
+
},
|
|
3240
|
+
]
|
|
3241
|
+
|
|
3242
|
+
// Files to scan
|
|
3243
|
+
const scanDirs = [
|
|
3244
|
+
join(cwd, '.mipham', 'skills'),
|
|
3245
|
+
join(cwd, '.mipham', 'rules'),
|
|
3246
|
+
join(cwd, '.claude'),
|
|
3247
|
+
]
|
|
3248
|
+
const skillFiles: string[] = []
|
|
3249
|
+
// Also check standard project files
|
|
3250
|
+
const projectFiles = [join(cwd, 'CLAUDE.md'), join(cwd, 'MIPHAM.md'), join(cwd, 'PRODUCT.md')]
|
|
3251
|
+
|
|
3252
|
+
// Collect skill/rules files
|
|
3253
|
+
for (const dir of scanDirs) {
|
|
3254
|
+
try {
|
|
3255
|
+
if (!existsSync(dir)) continue
|
|
3256
|
+
const walk = (d: string) => {
|
|
3257
|
+
const entries = readdirSync(d)
|
|
3258
|
+
for (const e of entries) {
|
|
3259
|
+
const fp = join(d, e)
|
|
3260
|
+
const st = statSync(fp)
|
|
3261
|
+
if (st.isDirectory()) {
|
|
3262
|
+
walk(fp)
|
|
3263
|
+
continue
|
|
3264
|
+
}
|
|
3265
|
+
if (['.md', '.SKILL.md', '.mipham-skill.md', '.txt'].some((ext) => fp.endsWith(ext))) {
|
|
3266
|
+
skillFiles.push(fp)
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
walk(dir)
|
|
3271
|
+
} catch {
|
|
3272
|
+
/* skip */
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
|
|
3276
|
+
const allFiles = [...new Set([...projectFiles.filter((f) => existsSync(f)), ...skillFiles])]
|
|
3277
|
+
|
|
3278
|
+
if (allFiles.length === 0) {
|
|
3279
|
+
return {
|
|
3280
|
+
content: [
|
|
3281
|
+
'🔍 Prompt Audit',
|
|
3282
|
+
'',
|
|
3283
|
+
'No prompt files found to audit.',
|
|
3284
|
+
'Place skills in .mipham/skills/ and rules in .mipham/rules/.',
|
|
3285
|
+
].join('\n'),
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
|
|
3289
|
+
// Scan each file
|
|
3290
|
+
const results: Array<{
|
|
3291
|
+
file: string
|
|
3292
|
+
line: number
|
|
3293
|
+
ruleId: string
|
|
3294
|
+
severity: string
|
|
3295
|
+
message: string
|
|
3296
|
+
suggestion: string
|
|
3297
|
+
}> = []
|
|
3298
|
+
|
|
3299
|
+
for (const fp of allFiles) {
|
|
3300
|
+
try {
|
|
3301
|
+
const content = readFileSync(fp, 'utf-8')
|
|
3302
|
+
const lines = content.split('\n')
|
|
3303
|
+
for (const rule of AUDIT_RULES) {
|
|
3304
|
+
// Multi-line pattern: check full content
|
|
3305
|
+
if (rule.id === 'do-not-list' || rule.id === 'example-overload') {
|
|
3306
|
+
if (rule.pattern.test(content)) {
|
|
3307
|
+
// Find approx line
|
|
3308
|
+
const match = rule.pattern.exec(content)
|
|
3309
|
+
const idx = match ? content.slice(0, match.index).split('\n').length : 1
|
|
3310
|
+
results.push({
|
|
3311
|
+
file: fp.replace(cwd + '/', ''),
|
|
3312
|
+
line: idx,
|
|
3313
|
+
ruleId: rule.id,
|
|
3314
|
+
severity: rule.severity,
|
|
3315
|
+
message: rule.message,
|
|
3316
|
+
suggestion: rule.suggestion,
|
|
3317
|
+
})
|
|
3318
|
+
}
|
|
3319
|
+
continue
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3322
|
+
// Line-by-line patterns
|
|
3323
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3324
|
+
const line = lines[i]!
|
|
3325
|
+
if (rule.pattern.test(line)) {
|
|
3326
|
+
results.push({
|
|
3327
|
+
file: fp.replace(cwd + '/', ''),
|
|
3328
|
+
line: i + 1,
|
|
3329
|
+
ruleId: rule.id,
|
|
3330
|
+
severity: rule.severity,
|
|
3331
|
+
message: rule.message,
|
|
3332
|
+
suggestion: rule.suggestion,
|
|
3333
|
+
})
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
} catch {
|
|
3338
|
+
/* skip unreadable files */
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
|
|
3342
|
+
// Format output
|
|
3343
|
+
const severityIcon: Record<string, string> = { high: '🔴', medium: '🟡', low: '🟢' }
|
|
3344
|
+
const grouped: Record<string, typeof results> = {}
|
|
3345
|
+
for (const r of results) {
|
|
3346
|
+
const key = r.file
|
|
3347
|
+
if (!grouped[key]) grouped[key] = []
|
|
3348
|
+
grouped[key]!.push(r)
|
|
3349
|
+
}
|
|
3350
|
+
|
|
3351
|
+
const output: string[] = [
|
|
3352
|
+
`🔍 Prompt Audit — ${allFiles.length} file(s) scanned, ${results.length} finding(s)`,
|
|
3353
|
+
'',
|
|
3354
|
+
]
|
|
3355
|
+
|
|
3356
|
+
if (results.length === 0) {
|
|
3357
|
+
output.push('✅ All prompts look optimized for modern models. No issues found.')
|
|
3358
|
+
} else {
|
|
3359
|
+
for (const [file, items] of Object.entries(grouped)) {
|
|
3360
|
+
output.push(`📄 ${file} (${items!.length} finding${items!.length > 1 ? 's' : ''})`)
|
|
3361
|
+
for (const item of items!) {
|
|
3362
|
+
output.push(
|
|
3363
|
+
` ${severityIcon[item.severity] || '⚪'} L${item.line}: [${item.ruleId}] ${item.message}`,
|
|
3364
|
+
)
|
|
3365
|
+
output.push(` → ${item.suggestion}`)
|
|
3366
|
+
}
|
|
3367
|
+
output.push('')
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
|
|
3371
|
+
output.push('─'.repeat(60))
|
|
3372
|
+
output.push('Scanned: ' + allFiles.map((f) => basename(f)).join(', '))
|
|
3373
|
+
|
|
3374
|
+
return { content: output.join('\n') }
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3055
3377
|
const securityCmd: CommandHandler = async () => {
|
|
3056
3378
|
const findings: string[] = []
|
|
3057
3379
|
const ok: string[] = []
|
|
@@ -3691,6 +4013,128 @@ Run \`mipham agents\` for the interactive dashboard.`,
|
|
|
3691
4013
|
// Artifacts
|
|
3692
4014
|
// ═══════════════════════════════════════════════════════════════
|
|
3693
4015
|
|
|
4016
|
+
const forkCmd: CommandHandler = async (ctx, args) => {
|
|
4017
|
+
const prompt = args.join(' ')
|
|
4018
|
+
if (!prompt.trim()) {
|
|
4019
|
+
return {
|
|
4020
|
+
content: [
|
|
4021
|
+
'Usage: /fork <task description>',
|
|
4022
|
+
'',
|
|
4023
|
+
'Fork a task into an isolated worktree with auto commit/push.',
|
|
4024
|
+
'',
|
|
4025
|
+
'Workflow:',
|
|
4026
|
+
' 1. Creates a new git worktree on branch worktree/<slug>',
|
|
4027
|
+
' 2. Spawns a background agent to work on the task',
|
|
4028
|
+
' 3. Auto-commits changes with Co-Authored-By on completion',
|
|
4029
|
+
' 4. Auto-pushes to origin',
|
|
4030
|
+
' 5. Reports branch and worktree path in the result',
|
|
4031
|
+
'',
|
|
4032
|
+
'Examples:',
|
|
4033
|
+
' /fork Refactor the auth module',
|
|
4034
|
+
' /fork Add input validation to all API endpoints',
|
|
4035
|
+
].join('\n'),
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
|
|
4039
|
+
const { execSync } = await import('node:child_process')
|
|
4040
|
+
const { join } = await import('node:path')
|
|
4041
|
+
|
|
4042
|
+
try {
|
|
4043
|
+
execSync('git rev-parse --git-dir', { stdio: 'ignore' })
|
|
4044
|
+
} catch {
|
|
4045
|
+
return { content: '/fork requires a git repository.' }
|
|
4046
|
+
}
|
|
4047
|
+
|
|
4048
|
+
const agentViewManager = ctx.engine.getAgentViewManager?.()
|
|
4049
|
+
if (!agentViewManager) {
|
|
4050
|
+
return { content: 'Agent View manager is initializing...' }
|
|
4051
|
+
}
|
|
4052
|
+
|
|
4053
|
+
const slug = prompt
|
|
4054
|
+
.toLowerCase()
|
|
4055
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
4056
|
+
.replace(/^-|-$/g, '')
|
|
4057
|
+
.slice(0, 40)
|
|
4058
|
+
const name = `${slug}-${Date.now().toString(36)}`
|
|
4059
|
+
const branch = `worktree/${name}`
|
|
4060
|
+
const wtPath = join(process.cwd(), '.claude', 'worktrees', name)
|
|
4061
|
+
|
|
4062
|
+
try {
|
|
4063
|
+
execSync(`git worktree add -b ${branch} ${wtPath} HEAD`, { stdio: 'ignore', timeout: 30_000 })
|
|
4064
|
+
} catch (err) {
|
|
4065
|
+
try {
|
|
4066
|
+
execSync(`git worktree remove --force ${wtPath}`, { stdio: 'ignore' })
|
|
4067
|
+
} catch {
|
|
4068
|
+
/* ok */
|
|
4069
|
+
}
|
|
4070
|
+
try {
|
|
4071
|
+
execSync(`git branch -D ${branch}`, { stdio: 'ignore' })
|
|
4072
|
+
} catch {
|
|
4073
|
+
/* ok */
|
|
4074
|
+
}
|
|
4075
|
+
return {
|
|
4076
|
+
content: `Worktree creation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
4079
|
+
|
|
4080
|
+
const session = agentViewManager.create(prompt, prompt, {
|
|
4081
|
+
provider: ctx.providerId,
|
|
4082
|
+
model: ctx.modelId,
|
|
4083
|
+
})
|
|
4084
|
+
agentViewManager.addMessage(session.id, { role: 'user', content: prompt })
|
|
4085
|
+
agentViewManager.updateStatus(session.id, 'working')
|
|
4086
|
+
session.worktree = wtPath
|
|
4087
|
+
session.branch = branch
|
|
4088
|
+
session.kind = 'forked'
|
|
4089
|
+
|
|
4090
|
+
const bgReg = (await import('../agent/background-registry')).getBackgroundAgentRegistry()
|
|
4091
|
+
bgReg.spawn(
|
|
4092
|
+
prompt,
|
|
4093
|
+
'general',
|
|
4094
|
+
async (_signal) => {
|
|
4095
|
+
const { SubAgent } = await import('../agent/sub-agent')
|
|
4096
|
+
const sa = new SubAgent(ctx.engine.getRegistry(), ctx.engine.getTools())
|
|
4097
|
+
const result = await sa.execute(prompt, 'fork: ' + prompt.slice(0, 60), {})
|
|
4098
|
+
try {
|
|
4099
|
+
const { execSync: ex } = await import('node:child_process')
|
|
4100
|
+
ex(`git -C ${wtPath} add -A`, { stdio: 'ignore', timeout: 10_000 })
|
|
4101
|
+
const st = ex(`git -C ${wtPath} status --porcelain`, { encoding: 'utf-8', timeout: 10_000 })
|
|
4102
|
+
if (st.trim()) {
|
|
4103
|
+
ex(
|
|
4104
|
+
`git -C ${wtPath} commit -m "feat: ${prompt.slice(0, 60)}\n\nCo-Authored-By: Claude <noreply@anthropic.com>"`,
|
|
4105
|
+
{ stdio: 'ignore', timeout: 10_000 },
|
|
4106
|
+
)
|
|
4107
|
+
ex(`git push origin ${branch}`, { stdio: 'ignore', timeout: 30_000 })
|
|
4108
|
+
}
|
|
4109
|
+
} catch {
|
|
4110
|
+
/* best-effort */
|
|
4111
|
+
}
|
|
4112
|
+
return [
|
|
4113
|
+
`## Fork done: ${prompt}`,
|
|
4114
|
+
'',
|
|
4115
|
+
`Branch: \`${branch}\``,
|
|
4116
|
+
`Worktree: \`${wtPath}\``,
|
|
4117
|
+
'',
|
|
4118
|
+
result || '(no output)',
|
|
4119
|
+
].join('\n')
|
|
4120
|
+
},
|
|
4121
|
+
'forked',
|
|
4122
|
+
)
|
|
4123
|
+
|
|
4124
|
+
return {
|
|
4125
|
+
content: [
|
|
4126
|
+
`✓ Forked: ${session.id}`,
|
|
4127
|
+
` Task: ${prompt.slice(0, 80)}`,
|
|
4128
|
+
` Branch: ${branch}`,
|
|
4129
|
+
` Worktree: ${wtPath}`,
|
|
4130
|
+
` Status: working`,
|
|
4131
|
+
'',
|
|
4132
|
+
'Agent will auto-commit + push on completion.',
|
|
4133
|
+
'Use /agents to track.',
|
|
4134
|
+
].join('\n'),
|
|
4135
|
+
}
|
|
4136
|
+
}
|
|
4137
|
+
|
|
3694
4138
|
async function openBrowser(url: string): Promise<void> {
|
|
3695
4139
|
const cmd =
|
|
3696
4140
|
process.platform === 'darwin'
|
|
@@ -3857,6 +4301,7 @@ const commandsListCmd: CommandHandler = () => {
|
|
|
3857
4301
|
'/recommend': 'Project',
|
|
3858
4302
|
'/security': 'Project',
|
|
3859
4303
|
'/audit': 'Project',
|
|
4304
|
+
'/prompt-audit': 'Code Quality',
|
|
3860
4305
|
'/ide': 'Environment',
|
|
3861
4306
|
'/terminal-setup': 'Environment',
|
|
3862
4307
|
'/memory': 'Environment',
|
|
@@ -3866,6 +4311,7 @@ const commandsListCmd: CommandHandler = () => {
|
|
|
3866
4311
|
'/feedback': 'Account',
|
|
3867
4312
|
'/agents': 'Agents',
|
|
3868
4313
|
'/bg': 'Agents',
|
|
4314
|
+
'/fork': 'Agents',
|
|
3869
4315
|
'/artifact': 'Artifacts',
|
|
3870
4316
|
'/schedule': 'Other',
|
|
3871
4317
|
'/commit': 'Workflow',
|
|
@@ -3977,6 +4423,7 @@ registry.set('/tasks', tasksCmd)
|
|
|
3977
4423
|
registry.set('/diff', diffCmd)
|
|
3978
4424
|
registry.set('/loop', loopCmd)
|
|
3979
4425
|
registry.set('/no-plan', noPlanCmd)
|
|
4426
|
+
registry.set('/workflow', workflowAutoCmd)
|
|
3980
4427
|
registry.set('/workflows', workflowsCmd)
|
|
3981
4428
|
registry.set('/review', reviewCmd)
|
|
3982
4429
|
registry.set('/pr-comments', prCommentsCmd)
|
|
@@ -3996,6 +4443,7 @@ registry.set('/permissions', permissionsCmd)
|
|
|
3996
4443
|
registry.set('/add-dir', addDirCmd)
|
|
3997
4444
|
registry.set('/security', securityCmd)
|
|
3998
4445
|
registry.set('/audit', securityCmd)
|
|
4446
|
+
registry.set('/prompt-audit', promptAuditCmd)
|
|
3999
4447
|
|
|
4000
4448
|
// Environment
|
|
4001
4449
|
registry.set('/theme', themeCmd)
|
|
@@ -4010,6 +4458,7 @@ registry.set('/logout', logoutCmd)
|
|
|
4010
4458
|
registry.set('/feedback', feedbackCmd)
|
|
4011
4459
|
registry.set('/agents', agentsCmd)
|
|
4012
4460
|
registry.set('/bg', bgCmd)
|
|
4461
|
+
registry.set('/fork', forkCmd)
|
|
4013
4462
|
|
|
4014
4463
|
// Artifacts
|
|
4015
4464
|
registry.set('/artifact', artifactBaseCmd)
|
|
@@ -4119,6 +4568,7 @@ const COMMAND_DESCRIPTIONS: Record<string, string> = {
|
|
|
4119
4568
|
'/recommend': 'Analyze project + recommend skills & setup',
|
|
4120
4569
|
'/security': 'Security review checklist',
|
|
4121
4570
|
'/audit': 'Same as /security',
|
|
4571
|
+
'/prompt-audit': 'Audit prompts for modern model optimization',
|
|
4122
4572
|
'/ide': 'IDE integration guide',
|
|
4123
4573
|
'/terminal-setup': 'Shell & terminal config',
|
|
4124
4574
|
'/memory': 'Manage AI memories',
|
|
@@ -4129,6 +4579,7 @@ const COMMAND_DESCRIPTIONS: Record<string, string> = {
|
|
|
4129
4579
|
'/feedback': 'Send feedback',
|
|
4130
4580
|
'/agents': 'Agent view dashboard',
|
|
4131
4581
|
'/bg': 'Run a background agent task',
|
|
4582
|
+
'/fork': 'Fork task into isolated worktree',
|
|
4132
4583
|
'/artifact': 'Manage artifacts',
|
|
4133
4584
|
'/schedule': 'View scheduled tasks and cron jobs',
|
|
4134
4585
|
'/commit': 'Generate commit message and review staged changes',
|