@miphamai/cli 0.11.0 → 0.12.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/src/artifacts/server.ts +39 -0
- package/src/config/defaults.ts +10 -1
- package/src/config/loader.ts +44 -2
- package/src/core/engine.ts +56 -1
- package/src/core/hooks.ts +23 -0
- package/src/core/inference-hook.ts +224 -0
- package/src/core/metrics.ts +364 -0
- package/src/index.tsx +9 -1
- package/src/shared/types.ts +57 -0
- package/src/ui/commands.ts +3 -4
- package/dist/mipham +0 -0
package/package.json
CHANGED
package/src/artifacts/server.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { ARTIFACT_ALLOWED_EXTENSIONS } from '../shared/constants'
|
|
|
5
5
|
import { readManifest } from './manifest'
|
|
6
6
|
import { ArtifactVersioning } from './versioning'
|
|
7
7
|
import type { ArtifactEntry } from '../shared/types'
|
|
8
|
+
import { getMetrics } from '../core/metrics'
|
|
8
9
|
|
|
9
10
|
// ── SSE client tracking ──
|
|
10
11
|
interface SseClient {
|
|
@@ -151,10 +152,48 @@ export class ArtifactServer {
|
|
|
151
152
|
return
|
|
152
153
|
}
|
|
153
154
|
|
|
155
|
+
// Metrics endpoint — Prometheus text format
|
|
156
|
+
if (urlPath === '/metrics') {
|
|
157
|
+
this.serveMetrics(res, 'prometheus')
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Metrics endpoint — JSON format
|
|
162
|
+
if (urlPath === '/metrics/json') {
|
|
163
|
+
this.serveMetrics(res, 'json')
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
|
|
154
167
|
// Artifact file
|
|
155
168
|
this.serveArtifact(urlPath, res)
|
|
156
169
|
}
|
|
157
170
|
|
|
171
|
+
// ── Metrics ──
|
|
172
|
+
|
|
173
|
+
private serveMetrics(res: any, format: 'prometheus' | 'json'): void {
|
|
174
|
+
try {
|
|
175
|
+
const metrics = getMetrics()
|
|
176
|
+
if (format === 'json') {
|
|
177
|
+
const body = JSON.stringify(metrics.toJSON(), null, 2)
|
|
178
|
+
res.writeHead(200, {
|
|
179
|
+
'Content-Type': 'application/json',
|
|
180
|
+
'Access-Control-Allow-Origin': '*',
|
|
181
|
+
})
|
|
182
|
+
res.end(body)
|
|
183
|
+
} else {
|
|
184
|
+
const body = metrics.toPrometheusText()
|
|
185
|
+
res.writeHead(200, {
|
|
186
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
187
|
+
'Access-Control-Allow-Origin': '*',
|
|
188
|
+
})
|
|
189
|
+
res.end(body)
|
|
190
|
+
}
|
|
191
|
+
} catch (err) {
|
|
192
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' })
|
|
193
|
+
res.end(`Metrics error: ${err instanceof Error ? err.message : 'unknown'}`)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
158
197
|
// ── SSE ──
|
|
159
198
|
|
|
160
199
|
private handleSse(res: any): void {
|
package/src/config/defaults.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MiphamConfig } from '../shared/index.ts'
|
|
1
|
+
import type { MiphamConfig, InferenceHookConfig } from '../shared/index.ts'
|
|
2
2
|
import { DEFAULT_PROVIDERS } from '../shared/index.ts'
|
|
3
3
|
import { PACKAGE_VERSION } from '../shared/index.ts'
|
|
4
4
|
|
|
@@ -9,3 +9,12 @@ export const DEFAULT_CONFIG: MiphamConfig = {
|
|
|
9
9
|
permission: 'auto',
|
|
10
10
|
providers: DEFAULT_PROVIDERS,
|
|
11
11
|
}
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_INFERENCE_HOOK_CONFIG: InferenceHookConfig = {
|
|
14
|
+
endpoint: '',
|
|
15
|
+
signing_secret: '',
|
|
16
|
+
timeout: 5000,
|
|
17
|
+
on_failure: 'fail-closed',
|
|
18
|
+
organization_id: '',
|
|
19
|
+
headers: {},
|
|
20
|
+
}
|
package/src/config/loader.ts
CHANGED
|
@@ -10,8 +10,13 @@ import {
|
|
|
10
10
|
import { join, dirname } from 'node:path'
|
|
11
11
|
import { homedir } from 'node:os'
|
|
12
12
|
import { parse as parseYaml } from 'yaml'
|
|
13
|
-
import type {
|
|
14
|
-
|
|
13
|
+
import type {
|
|
14
|
+
MiphamConfig,
|
|
15
|
+
ProviderConfig,
|
|
16
|
+
McpServerConfig,
|
|
17
|
+
InferenceHookConfig,
|
|
18
|
+
} from '../shared/index.ts'
|
|
19
|
+
import { DEFAULT_CONFIG, DEFAULT_INFERENCE_HOOK_CONFIG } from './defaults'
|
|
15
20
|
|
|
16
21
|
const MIPHAM_HOME = join(homedir(), '.mipham')
|
|
17
22
|
const BACKUP_PREFIX = 'config.backup-'
|
|
@@ -256,3 +261,40 @@ export function loadConfig(cwd: string = process.cwd()): MiphamConfig {
|
|
|
256
261
|
|
|
257
262
|
return config
|
|
258
263
|
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Load inference hooks (DLP) configuration from the same config sources
|
|
267
|
+
* as the main config. Merges project-level over user-level.
|
|
268
|
+
*
|
|
269
|
+
* Returns default (disabled) config if no inference_hooks section is present.
|
|
270
|
+
*/
|
|
271
|
+
export function loadInferenceHookConfig(cwd: string = process.cwd()): InferenceHookConfig {
|
|
272
|
+
const configPath = join(cwd, '.mipham', 'config.yml')
|
|
273
|
+
const userConfigPath = join(MIPHAM_HOME, 'config.yml')
|
|
274
|
+
|
|
275
|
+
let merged = { ...DEFAULT_INFERENCE_HOOK_CONFIG }
|
|
276
|
+
|
|
277
|
+
const paths = [userConfigPath, configPath] // project wins (loaded last)
|
|
278
|
+
for (const path of paths) {
|
|
279
|
+
try {
|
|
280
|
+
if (!existsSync(path)) continue
|
|
281
|
+
const raw = readFileSync(path, 'utf-8')
|
|
282
|
+
const parsed = parseYaml(raw) as Record<string, unknown>
|
|
283
|
+
const section = parsed.inference_hooks as Partial<InferenceHookConfig> | undefined
|
|
284
|
+
if (section) {
|
|
285
|
+
merged = {
|
|
286
|
+
endpoint: section.endpoint ?? merged.endpoint,
|
|
287
|
+
signing_secret: section.signing_secret ?? merged.signing_secret,
|
|
288
|
+
timeout: section.timeout ?? merged.timeout,
|
|
289
|
+
on_failure: section.on_failure ?? merged.on_failure,
|
|
290
|
+
organization_id: section.organization_id ?? merged.organization_id,
|
|
291
|
+
headers: { ...merged.headers, ...(section.headers || {}) },
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
} catch {
|
|
295
|
+
// Silently skip malformed configs — main loadConfig already warns
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return merged
|
|
300
|
+
}
|
package/src/core/engine.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
StreamChunk,
|
|
3
|
+
ToolDefinition,
|
|
4
|
+
ToolResult,
|
|
5
|
+
InferenceHookConfig,
|
|
6
|
+
} from '../shared/index.ts'
|
|
2
7
|
import { ProviderRegistry } from '../providers/registry'
|
|
3
8
|
import { ContextManager } from './context'
|
|
4
9
|
import { PermissionSystem } from './permission'
|
|
@@ -11,6 +16,7 @@ import type { AgentViewManager } from '../agent-view/agent-view-manager'
|
|
|
11
16
|
import type { SkillsLoader } from '../skills/loader'
|
|
12
17
|
import { getBackgroundAgentRegistry } from '../agent/background-registry'
|
|
13
18
|
import { RulesLoader } from './rules-loader'
|
|
19
|
+
import { buildRequest, sendInferenceCheck, isInferenceHookEnabled } from './inference-hook'
|
|
14
20
|
|
|
15
21
|
export class QueryEngine {
|
|
16
22
|
private hookEngine?: HookEngine
|
|
@@ -76,6 +82,8 @@ export class QueryEngine {
|
|
|
76
82
|
private rulesLoader?: RulesLoader
|
|
77
83
|
/** Files touched in the current turn (for rules matching). */
|
|
78
84
|
private touchedFiles: Set<string> = new Set()
|
|
85
|
+
/** Inference hook (DLP) configuration. */
|
|
86
|
+
private inferenceHookConfig?: InferenceHookConfig
|
|
79
87
|
|
|
80
88
|
/** Register the rules loader. */
|
|
81
89
|
setRulesLoader(loader: RulesLoader): void {
|
|
@@ -83,6 +91,11 @@ export class QueryEngine {
|
|
|
83
91
|
this.rulesLoader.load()
|
|
84
92
|
}
|
|
85
93
|
|
|
94
|
+
/** Register inference hook (DLP) configuration. */
|
|
95
|
+
setInferenceHookConfig(config: InferenceHookConfig): void {
|
|
96
|
+
this.inferenceHookConfig = config
|
|
97
|
+
}
|
|
98
|
+
|
|
86
99
|
/** Pending task notifications from background agents (cleared after draining). */
|
|
87
100
|
private pendingTaskNotifications: Array<StreamChunk> = []
|
|
88
101
|
|
|
@@ -256,6 +269,27 @@ export class QueryEngine {
|
|
|
256
269
|
const messages = this.context.getMessages()
|
|
257
270
|
const toolDefs = this.getToolDefinitions()
|
|
258
271
|
|
|
272
|
+
// ── PreInference DLP checkpoint ──
|
|
273
|
+
if (isInferenceHookEnabled(this.inferenceHookConfig)) {
|
|
274
|
+
const provider = this.registry.getActive().config.id
|
|
275
|
+
const model = this.registry.getActiveModel()
|
|
276
|
+
const request = buildRequest(
|
|
277
|
+
messages,
|
|
278
|
+
'session-1',
|
|
279
|
+
provider,
|
|
280
|
+
model,
|
|
281
|
+
this.inferenceHookConfig!.organization_id,
|
|
282
|
+
)
|
|
283
|
+
const verdict = await sendInferenceCheck(this.inferenceHookConfig!, request)
|
|
284
|
+
if (!verdict.allowed) {
|
|
285
|
+
yield {
|
|
286
|
+
type: 'error',
|
|
287
|
+
error: verdict.reason || 'Request blocked by DLP policy.',
|
|
288
|
+
}
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
259
293
|
let assistantContent = ''
|
|
260
294
|
let reasoningContent = ''
|
|
261
295
|
let thinkingContent = ''
|
|
@@ -481,6 +515,27 @@ export class QueryEngine {
|
|
|
481
515
|
const systemPrompt = this.context.getSystemPrompt()
|
|
482
516
|
const messages = this.context.getMessages()
|
|
483
517
|
|
|
518
|
+
// ── PreInference DLP checkpoint (every tool-calling turn) ──
|
|
519
|
+
if (isInferenceHookEnabled(this.inferenceHookConfig)) {
|
|
520
|
+
const provider = this.registry.getActive().config.id
|
|
521
|
+
const model = this.registry.getActiveModel()
|
|
522
|
+
const request = buildRequest(
|
|
523
|
+
messages,
|
|
524
|
+
'session-1',
|
|
525
|
+
provider,
|
|
526
|
+
model,
|
|
527
|
+
this.inferenceHookConfig!.organization_id,
|
|
528
|
+
)
|
|
529
|
+
const verdict = await sendInferenceCheck(this.inferenceHookConfig!, request)
|
|
530
|
+
if (!verdict.allowed) {
|
|
531
|
+
yield {
|
|
532
|
+
type: 'error',
|
|
533
|
+
error: verdict.reason || 'Request blocked by DLP policy.',
|
|
534
|
+
}
|
|
535
|
+
return
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
484
539
|
let assistantContent = ''
|
|
485
540
|
let reasoningContent = ''
|
|
486
541
|
let thinkingContent = ''
|
package/src/core/hooks.ts
CHANGED
|
@@ -155,6 +155,29 @@ export class HookEngine {
|
|
|
155
155
|
return this.runHooks('PostToolUseFailure', toolName, ctx)
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
/** PreInference: fires before every model API call for DLP inspection. */
|
|
159
|
+
async executePreInference(
|
|
160
|
+
messages: Array<{ role: string; content: string }>,
|
|
161
|
+
toolCalls: Array<{
|
|
162
|
+
name: string
|
|
163
|
+
input: Record<string, unknown>
|
|
164
|
+
resultPreview: string
|
|
165
|
+
}>,
|
|
166
|
+
sessionId: string,
|
|
167
|
+
provider: string,
|
|
168
|
+
model: string,
|
|
169
|
+
): Promise<HookResult> {
|
|
170
|
+
const ctx: HookContext = {
|
|
171
|
+
event: 'PreInference',
|
|
172
|
+
sessionId,
|
|
173
|
+
messages,
|
|
174
|
+
toolCalls,
|
|
175
|
+
provider,
|
|
176
|
+
model,
|
|
177
|
+
}
|
|
178
|
+
return this.runHooks('PreInference', undefined, ctx)
|
|
179
|
+
}
|
|
180
|
+
|
|
158
181
|
// ── Core execution ──
|
|
159
182
|
|
|
160
183
|
private async runHooks(
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { createHmac, randomUUID } from 'node:crypto'
|
|
2
|
+
import type {
|
|
3
|
+
InferenceHookConfig,
|
|
4
|
+
InferenceCheckRequest,
|
|
5
|
+
InferenceCheckResponse,
|
|
6
|
+
Message,
|
|
7
|
+
} from '../shared/index.ts'
|
|
8
|
+
|
|
9
|
+
// ── Result type ──
|
|
10
|
+
|
|
11
|
+
export interface InferenceVerdict {
|
|
12
|
+
allowed: boolean
|
|
13
|
+
reason?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ── Public API ──
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Build the inference-check request payload from current conversation state.
|
|
20
|
+
*
|
|
21
|
+
* Follows Claude Inference Hooks protocol:
|
|
22
|
+
* - Sends all messages EXCEPT system role (never expose system prompts)
|
|
23
|
+
* - Includes recent tool calls with result previews (truncated to 2000 chars)
|
|
24
|
+
* - Omits tool definitions and raw file/image content
|
|
25
|
+
*/
|
|
26
|
+
export function buildRequest(
|
|
27
|
+
messages: Message[],
|
|
28
|
+
sessionId: string,
|
|
29
|
+
provider: string,
|
|
30
|
+
model: string,
|
|
31
|
+
organizationId?: string,
|
|
32
|
+
): InferenceCheckRequest {
|
|
33
|
+
// Filter: exclude system messages, flatten content blocks to text
|
|
34
|
+
const serialized: Array<{ role: string; content: string }> = []
|
|
35
|
+
for (const msg of messages) {
|
|
36
|
+
if (msg.role === 'system') continue
|
|
37
|
+
serialized.push({
|
|
38
|
+
role: msg.role,
|
|
39
|
+
content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Extract tool calls from the message history
|
|
44
|
+
const toolCalls = extractToolCalls(messages)
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
type: 'inference_check',
|
|
48
|
+
id: `evt_${randomUUID()}`,
|
|
49
|
+
created_at: new Date().toISOString(),
|
|
50
|
+
data: {
|
|
51
|
+
type: 'pre_inference',
|
|
52
|
+
session_id: sessionId,
|
|
53
|
+
organization_id: organizationId || undefined,
|
|
54
|
+
provider,
|
|
55
|
+
model,
|
|
56
|
+
messages: serialized,
|
|
57
|
+
tool_calls: toolCalls,
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Send the inference-check request to the DLP server and return the verdict.
|
|
64
|
+
*
|
|
65
|
+
* On network error or timeout, applies the configured `on_failure` strategy:
|
|
66
|
+
* - 'fail-closed': block the request (security-first)
|
|
67
|
+
* - 'fail-open': allow the request (availability-first)
|
|
68
|
+
*/
|
|
69
|
+
export async function sendInferenceCheck(
|
|
70
|
+
config: InferenceHookConfig,
|
|
71
|
+
request: InferenceCheckRequest,
|
|
72
|
+
): Promise<InferenceVerdict> {
|
|
73
|
+
const body = JSON.stringify(request)
|
|
74
|
+
|
|
75
|
+
// Build signature header
|
|
76
|
+
const signature = signPayload(config.signing_secret, body)
|
|
77
|
+
|
|
78
|
+
const headers: Record<string, string> = {
|
|
79
|
+
'Content-Type': 'application/json',
|
|
80
|
+
'User-Agent': `MiphamCode/${getVersion()}`,
|
|
81
|
+
...(config.headers || {}),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (signature) {
|
|
85
|
+
headers['X-Mipham-Signature'] = signature
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const response = await fetch(config.endpoint, {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
headers,
|
|
92
|
+
body,
|
|
93
|
+
signal: AbortSignal.timeout(config.timeout),
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const responseBody = await response.text()
|
|
97
|
+
|
|
98
|
+
if (response.status === 200) {
|
|
99
|
+
// Parse verdict — treat any non-deny as allow
|
|
100
|
+
try {
|
|
101
|
+
const parsed = JSON.parse(responseBody) as InferenceCheckResponse
|
|
102
|
+
if (parsed.verdict === 'deny') {
|
|
103
|
+
return { allowed: false, reason: parsed.reason || 'Blocked by DLP policy' }
|
|
104
|
+
}
|
|
105
|
+
return { allowed: true }
|
|
106
|
+
} catch {
|
|
107
|
+
// Unparseable response — treat as allow (server acknowledged receipt)
|
|
108
|
+
return { allowed: true }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 403 = explicit deny
|
|
113
|
+
if (response.status === 403) {
|
|
114
|
+
try {
|
|
115
|
+
const parsed = JSON.parse(responseBody) as InferenceCheckResponse
|
|
116
|
+
return { allowed: false, reason: parsed.reason || 'Blocked by DLP policy' }
|
|
117
|
+
} catch {
|
|
118
|
+
return { allowed: false, reason: `DLP server denied (403): ${responseBody.slice(0, 200)}` }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Other non-2xx: apply failure posture
|
|
123
|
+
if (config.on_failure === 'fail-closed') {
|
|
124
|
+
return {
|
|
125
|
+
allowed: false,
|
|
126
|
+
reason: `DLP server returned ${response.status}: ${responseBody.slice(0, 200)}`,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return { allowed: true }
|
|
130
|
+
} catch (err) {
|
|
131
|
+
// Network error or timeout
|
|
132
|
+
if (config.on_failure === 'fail-closed') {
|
|
133
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
134
|
+
return {
|
|
135
|
+
allowed: false,
|
|
136
|
+
reason: `DLP server unreachable: ${msg}`,
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// fail-open: allow through
|
|
140
|
+
return { allowed: true }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Check if inference hook is configured and should be used.
|
|
146
|
+
*/
|
|
147
|
+
export function isInferenceHookEnabled(config?: InferenceHookConfig): boolean {
|
|
148
|
+
return !!(config?.endpoint && config.endpoint.length > 0)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Internal helpers ──
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Extract tool calls and their results from the entire message history.
|
|
155
|
+
* Scans for tool_use/tool_result pairs across all messages.
|
|
156
|
+
* Each result_preview is truncated to 2000 characters.
|
|
157
|
+
*/
|
|
158
|
+
function extractToolCalls(
|
|
159
|
+
messages: Message[],
|
|
160
|
+
): Array<{ name: string; input: Record<string, unknown>; result_preview: string }> {
|
|
161
|
+
// Collect tool results first (keyed by tool_use_id)
|
|
162
|
+
const toolResults = new Map<string, string>()
|
|
163
|
+
for (const msg of messages) {
|
|
164
|
+
if (Array.isArray(msg.content)) {
|
|
165
|
+
for (const block of msg.content) {
|
|
166
|
+
if (block.type === 'tool_result') {
|
|
167
|
+
toolResults.set(block.tool_use_id, block.content || '')
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Extract tool_use blocks with their result previews
|
|
174
|
+
const calls: Array<{
|
|
175
|
+
name: string
|
|
176
|
+
input: Record<string, unknown>
|
|
177
|
+
result_preview: string
|
|
178
|
+
}> = []
|
|
179
|
+
|
|
180
|
+
for (const msg of messages) {
|
|
181
|
+
if (Array.isArray(msg.content)) {
|
|
182
|
+
for (const block of msg.content) {
|
|
183
|
+
if (block.type === 'tool_use') {
|
|
184
|
+
const resultPreview = toolResults.get(block.id) || ''
|
|
185
|
+
calls.push({
|
|
186
|
+
name: block.name,
|
|
187
|
+
input: block.input,
|
|
188
|
+
result_preview: resultPreview.slice(0, 2000),
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return calls
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Sign the request body using HMAC-SHA256.
|
|
200
|
+
* Follows Standard Webhooks specification:
|
|
201
|
+
* X-Mipham-Signature: t=<unix_timestamp>,v1=<hmac_sha256_hex>
|
|
202
|
+
* Returns empty string if no signing secret is configured.
|
|
203
|
+
*/
|
|
204
|
+
function signPayload(secret: string, body: string): string {
|
|
205
|
+
if (!secret) return ''
|
|
206
|
+
const timestamp = Math.floor(Date.now() / 1000).toString()
|
|
207
|
+
const signedPayload = `${timestamp}.${body}`
|
|
208
|
+
const signature = createHmac('sha256', secret).update(signedPayload).digest('hex')
|
|
209
|
+
return `t=${timestamp},v1=${signature}`
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Get the current package version for the User-Agent header.
|
|
214
|
+
*/
|
|
215
|
+
function getVersion(): string {
|
|
216
|
+
try {
|
|
217
|
+
// Dynamic import to avoid circular deps — the shared package-info
|
|
218
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
219
|
+
const pkg = require('../../package.json') as { version?: string }
|
|
220
|
+
return pkg.version || '0.0.0'
|
|
221
|
+
} catch {
|
|
222
|
+
return '0.0.0'
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight Prometheus-compatible metrics registry for mipham-code.
|
|
3
|
+
*
|
|
4
|
+
* Provides Counter, Gauge, and Histogram metric types compatible with
|
|
5
|
+
* the Python shared/metrics.py module in MegaSystem. Metrics are
|
|
6
|
+
* collected in-memory and can be exported in Prometheus text format
|
|
7
|
+
* or JSON.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* import { getMetrics } from '../core/metrics.js'
|
|
11
|
+
*
|
|
12
|
+
* const m = getMetrics()
|
|
13
|
+
* m.cliInvocations.inc()
|
|
14
|
+
* m.toolCalls.inc({ tool_name: 'bash' })
|
|
15
|
+
* m.modelRequestDuration.observe(150.0, { provider: 'anthropic' })
|
|
16
|
+
*
|
|
17
|
+
* Endpoints (via ArtifactServer):
|
|
18
|
+
* GET /metrics — Prometheus text format
|
|
19
|
+
* GET /metrics/json — JSON format
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// ── Metric types ──────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
export interface MetricLabels {
|
|
25
|
+
[key: string]: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Format labels into Prometheus {...} string. */
|
|
29
|
+
function formatLabels(labels?: MetricLabels): string {
|
|
30
|
+
if (!labels || Object.keys(labels).length === 0) return ''
|
|
31
|
+
const parts = Object.entries(labels)
|
|
32
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
33
|
+
.map(([k, v]) => `${k}="${v.replace(/"/g, '\\"')}"`)
|
|
34
|
+
return `{${parts.join(',')}}`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── Counter ──────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
export class Counter {
|
|
40
|
+
readonly name: string
|
|
41
|
+
readonly help: string
|
|
42
|
+
readonly labels: MetricLabels
|
|
43
|
+
private _value = 0
|
|
44
|
+
|
|
45
|
+
constructor(name: string, help: string, labels?: MetricLabels) {
|
|
46
|
+
this.name = name
|
|
47
|
+
this.help = help
|
|
48
|
+
this.labels = labels ?? {}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
inc(amount = 1): void {
|
|
52
|
+
this._value += amount
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get value(): number {
|
|
56
|
+
return this._value
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Full metric name with labels for dedup key. */
|
|
60
|
+
get key(): string {
|
|
61
|
+
return this.name + formatLabels(this.labels)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
toPrometheus(): string {
|
|
65
|
+
const labelStr = formatLabels(this.labels)
|
|
66
|
+
return `${this.name}${labelStr} ${this._value}`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
toJSON(): object {
|
|
70
|
+
return {
|
|
71
|
+
name: this.name,
|
|
72
|
+
type: 'counter',
|
|
73
|
+
help: this.help,
|
|
74
|
+
labels: this.labels,
|
|
75
|
+
value: this._value,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Gauge ────────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
export class Gauge {
|
|
83
|
+
readonly name: string
|
|
84
|
+
readonly help: string
|
|
85
|
+
readonly labels: MetricLabels
|
|
86
|
+
private _value = 0
|
|
87
|
+
|
|
88
|
+
constructor(name: string, help: string, labels?: MetricLabels) {
|
|
89
|
+
this.name = name
|
|
90
|
+
this.help = help
|
|
91
|
+
this.labels = labels ?? {}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
inc(amount = 1): void {
|
|
95
|
+
this._value += amount
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
dec(amount = 1): void {
|
|
99
|
+
this._value -= amount
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
set(value: number): void {
|
|
103
|
+
this._value = value
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
get value(): number {
|
|
107
|
+
return this._value
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
get key(): string {
|
|
111
|
+
return this.name + formatLabels(this.labels)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
toPrometheus(): string {
|
|
115
|
+
const labelStr = formatLabels(this.labels)
|
|
116
|
+
return `${this.name}${labelStr} ${this._value}`
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
toJSON(): object {
|
|
120
|
+
return {
|
|
121
|
+
name: this.name,
|
|
122
|
+
type: 'gauge',
|
|
123
|
+
help: this.help,
|
|
124
|
+
labels: this.labels,
|
|
125
|
+
value: this._value,
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Histogram ────────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
const DEFAULT_BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
|
|
133
|
+
|
|
134
|
+
export class Histogram {
|
|
135
|
+
readonly name: string
|
|
136
|
+
readonly help: string
|
|
137
|
+
readonly labels: MetricLabels
|
|
138
|
+
readonly buckets: number[]
|
|
139
|
+
private _count = 0
|
|
140
|
+
private _sum = 0
|
|
141
|
+
private _bucketCounts: number[]
|
|
142
|
+
|
|
143
|
+
constructor(name: string, help: string, buckets?: number[], labels?: MetricLabels) {
|
|
144
|
+
this.name = name
|
|
145
|
+
this.help = help
|
|
146
|
+
this.labels = labels ?? {}
|
|
147
|
+
this.buckets = buckets ?? DEFAULT_BUCKETS
|
|
148
|
+
this._bucketCounts = new Array(this.buckets.length).fill(0)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
observe(value: number): void {
|
|
152
|
+
this._count++
|
|
153
|
+
this._sum += value
|
|
154
|
+
for (let i = 0; i < this.buckets.length; i++) {
|
|
155
|
+
if (value <= (this.buckets[i] ?? Infinity)) {
|
|
156
|
+
this._bucketCounts[i] = (this._bucketCounts[i] ?? 0) + 1
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
get count(): number {
|
|
162
|
+
return this._count
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
get sum(): number {
|
|
166
|
+
return this._sum
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
get key(): string {
|
|
170
|
+
return this.name + formatLabels(this.labels)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
toPrometheus(): string {
|
|
174
|
+
const labelStr = formatLabels(this.labels)
|
|
175
|
+
const lines: string[] = []
|
|
176
|
+
|
|
177
|
+
// _bucket values
|
|
178
|
+
for (let i = 0; i < this.buckets.length; i++) {
|
|
179
|
+
const bucketLabel = formatLabels({
|
|
180
|
+
...this.labels,
|
|
181
|
+
le: String(this.buckets[i]),
|
|
182
|
+
})
|
|
183
|
+
lines.push(`${this.name}_bucket${bucketLabel} ${this._bucketCounts[i]}`)
|
|
184
|
+
}
|
|
185
|
+
// +Inf bucket
|
|
186
|
+
const infLabel = formatLabels({ ...this.labels, le: '+Inf' })
|
|
187
|
+
lines.push(`${this.name}_bucket${infLabel} ${this._count}`)
|
|
188
|
+
|
|
189
|
+
// _sum and _count
|
|
190
|
+
lines.push(`${this.name}_sum${labelStr} ${this._sum}`)
|
|
191
|
+
lines.push(`${this.name}_count${labelStr} ${this._count}`)
|
|
192
|
+
|
|
193
|
+
return lines.join('\n')
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
toJSON(): object {
|
|
197
|
+
const bucketResults: { le: string; count: number }[] = []
|
|
198
|
+
for (let i = 0; i < this.buckets.length; i++) {
|
|
199
|
+
bucketResults.push({ le: String(this.buckets[i]), count: this._bucketCounts[i] ?? 0 })
|
|
200
|
+
}
|
|
201
|
+
bucketResults.push({ le: '+Inf', count: this._count })
|
|
202
|
+
return {
|
|
203
|
+
name: this.name,
|
|
204
|
+
type: 'histogram',
|
|
205
|
+
help: this.help,
|
|
206
|
+
labels: this.labels,
|
|
207
|
+
count: this._count,
|
|
208
|
+
sum: this._sum,
|
|
209
|
+
buckets: bucketResults,
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── MetricsRegistry ──────────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
export class MetricsRegistry {
|
|
217
|
+
private _counters = new Map<string, Counter>()
|
|
218
|
+
private _gauges = new Map<string, Gauge>()
|
|
219
|
+
private _histograms = new Map<string, Histogram>()
|
|
220
|
+
|
|
221
|
+
// ── Predefined metrics ─────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
/** CLI invocation counter. Incremented on each CLI startup. */
|
|
224
|
+
readonly cliInvocations: Counter
|
|
225
|
+
|
|
226
|
+
/** Tool call counter, labelled by tool_name. Callers use .inc({tool_name}). */
|
|
227
|
+
readonly toolCalls: Counter
|
|
228
|
+
|
|
229
|
+
/** Model API request counter, labelled by provider and model. */
|
|
230
|
+
readonly modelRequests: Counter
|
|
231
|
+
|
|
232
|
+
/** Model API request error counter, labelled by provider and error type. */
|
|
233
|
+
readonly modelRequestErrors: Counter
|
|
234
|
+
|
|
235
|
+
/** Model API request latency histogram (milliseconds). */
|
|
236
|
+
readonly modelRequestDurationMs: Histogram
|
|
237
|
+
|
|
238
|
+
/** Active CLI sessions gauge. */
|
|
239
|
+
readonly activeSessions: Gauge
|
|
240
|
+
|
|
241
|
+
constructor() {
|
|
242
|
+
// Pre-register standard metrics
|
|
243
|
+
this.cliInvocations = this.counter(
|
|
244
|
+
'mipham_code_cli_invocations_total',
|
|
245
|
+
'Number of CLI invocations',
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
this.toolCalls = this.counter('mipham_code_tool_calls_total', 'Number of tool invocations')
|
|
249
|
+
|
|
250
|
+
this.modelRequests = this.counter(
|
|
251
|
+
'mipham_code_model_requests_total',
|
|
252
|
+
'Number of model API requests',
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
this.modelRequestErrors = this.counter(
|
|
256
|
+
'mipham_code_model_request_errors_total',
|
|
257
|
+
'Number of model API request errors',
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
this.modelRequestDurationMs = this.histogram(
|
|
261
|
+
'mipham_code_model_request_duration_ms',
|
|
262
|
+
'Model API request duration in milliseconds',
|
|
263
|
+
[50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000, 60000],
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
this.activeSessions = this.gauge('mipham_code_active_sessions', 'Number of active CLI sessions')
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ── Factory methods ─────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
counter(name: string, help: string, labels?: MetricLabels): Counter {
|
|
272
|
+
const c = new Counter(name, help, labels)
|
|
273
|
+
if (this._counters.has(c.key)) return this._counters.get(c.key)!
|
|
274
|
+
this._counters.set(c.key, c)
|
|
275
|
+
return c
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
gauge(name: string, help: string, labels?: MetricLabels): Gauge {
|
|
279
|
+
const g = new Gauge(name, help, labels)
|
|
280
|
+
if (this._gauges.has(g.key)) return this._gauges.get(g.key)!
|
|
281
|
+
this._gauges.set(g.key, g)
|
|
282
|
+
return g
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
histogram(name: string, help: string, buckets?: number[], labels?: MetricLabels): Histogram {
|
|
286
|
+
const h = new Histogram(name, help, buckets, labels)
|
|
287
|
+
if (this._histograms.has(h.key)) return this._histograms.get(h.key)!
|
|
288
|
+
this._histograms.set(h.key, h)
|
|
289
|
+
return h
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ── Export ──────────────────────────────────────────────────────
|
|
293
|
+
|
|
294
|
+
/** Export all metrics in Prometheus text format. */
|
|
295
|
+
toPrometheusText(): string {
|
|
296
|
+
const lines: string[] = []
|
|
297
|
+
const seen = new Set<string>()
|
|
298
|
+
|
|
299
|
+
for (const c of this._counters.values()) {
|
|
300
|
+
if (!seen.has(c.name)) {
|
|
301
|
+
lines.push(`# HELP ${c.name} ${c.help}`)
|
|
302
|
+
lines.push(`# TYPE ${c.name} counter`)
|
|
303
|
+
seen.add(c.name)
|
|
304
|
+
}
|
|
305
|
+
lines.push(c.toPrometheus())
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
for (const g of this._gauges.values()) {
|
|
309
|
+
if (!seen.has(g.name)) {
|
|
310
|
+
lines.push(`# HELP ${g.name} ${g.help}`)
|
|
311
|
+
lines.push(`# TYPE ${g.name} gauge`)
|
|
312
|
+
seen.add(g.name)
|
|
313
|
+
}
|
|
314
|
+
lines.push(g.toPrometheus())
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
for (const h of this._histograms.values()) {
|
|
318
|
+
if (!seen.has(h.name)) {
|
|
319
|
+
lines.push(`# HELP ${h.name} ${h.help}`)
|
|
320
|
+
lines.push(`# TYPE ${h.name} histogram`)
|
|
321
|
+
seen.add(h.name)
|
|
322
|
+
}
|
|
323
|
+
lines.push(h.toPrometheus())
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return lines.join('\n') + '\n'
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Export all metrics as JSON. */
|
|
330
|
+
toJSON(): object {
|
|
331
|
+
return {
|
|
332
|
+
counters: Array.from(this._counters.values()).map((c) => c.toJSON()),
|
|
333
|
+
gauges: Array.from(this._gauges.values()).map((g) => g.toJSON()),
|
|
334
|
+
histograms: Array.from(this._histograms.values()).map((h) => h.toJSON()),
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Reset all metrics (useful for testing). */
|
|
339
|
+
reset(): void {
|
|
340
|
+
this._counters.clear()
|
|
341
|
+
this._gauges.clear()
|
|
342
|
+
this._histograms.clear()
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ── Singleton ────────────────────────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
let _instance: MetricsRegistry | null = null
|
|
349
|
+
|
|
350
|
+
/** Get the global MetricsRegistry singleton. */
|
|
351
|
+
export function getMetrics(): MetricsRegistry {
|
|
352
|
+
if (!_instance) {
|
|
353
|
+
_instance = new MetricsRegistry()
|
|
354
|
+
}
|
|
355
|
+
return _instance
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Reset the singleton (useful for testing). */
|
|
359
|
+
export function resetMetrics(): void {
|
|
360
|
+
if (_instance) {
|
|
361
|
+
_instance.reset()
|
|
362
|
+
_instance = null
|
|
363
|
+
}
|
|
364
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
2
|
import { render } from 'ink'
|
|
3
3
|
import { App } from './ui/app'
|
|
4
|
-
import { loadConfig } from './config/loader'
|
|
4
|
+
import { loadConfig, loadInferenceHookConfig } from './config/loader'
|
|
5
5
|
import { bootstrapProviders } from './providers/bootstrap'
|
|
6
6
|
import { InstructionsLoader } from './core/instructions'
|
|
7
7
|
import { loadSessionMemories } from './core/memory/memory-loader'
|
|
@@ -18,6 +18,7 @@ import { registerMcpServerTools } from './mcp/registry'
|
|
|
18
18
|
import { AgentRegistry } from './agent/agent-registry'
|
|
19
19
|
import { HookEngine } from './core/hooks'
|
|
20
20
|
import { ArtifactServer } from './artifacts/server'
|
|
21
|
+
import { getMetrics } from './core/metrics'
|
|
21
22
|
import { ARTIFACTS_DIR, ARTIFACT_PORT, MIPHAM_DIR } from './shared/constants'
|
|
22
23
|
import { AgentViewManager } from './agent-view/agent-view-manager'
|
|
23
24
|
import { AgentViewDashboard } from './agent-view/dashboard'
|
|
@@ -32,6 +33,9 @@ interface RunOptions {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export async function runApp(options: RunOptions): Promise<void> {
|
|
36
|
+
// Metrics: count CLI invocation
|
|
37
|
+
getMetrics().cliInvocations.inc()
|
|
38
|
+
|
|
35
39
|
// Handle `mipham agents` subcommand — launch standalone dashboard
|
|
36
40
|
const args = process.argv.slice(2)
|
|
37
41
|
if (args[0] === 'agents') {
|
|
@@ -149,6 +153,10 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
149
153
|
engine.setAgentViewManager(agentViewManager)
|
|
150
154
|
engine.setSkillsLoader(skillsLoader)
|
|
151
155
|
|
|
156
|
+
// Wire inference hooks (DLP) configuration
|
|
157
|
+
const inferenceHookConfig = loadInferenceHookConfig()
|
|
158
|
+
engine.setInferenceHookConfig(inferenceHookConfig)
|
|
159
|
+
|
|
152
160
|
// Sync engine permission with config (fix: UI shows "auto" but engine defaulted to bypass-legacy)
|
|
153
161
|
if (config.permission) {
|
|
154
162
|
engine.getPermission().setDefaultLevel(config.permission as PermissionLevel)
|
package/src/shared/types.ts
CHANGED
|
@@ -196,6 +196,7 @@ export type HookEvent =
|
|
|
196
196
|
| 'ConfigChange'
|
|
197
197
|
| 'SubagentStart'
|
|
198
198
|
| 'SubagentStop'
|
|
199
|
+
| 'PreInference'
|
|
199
200
|
|
|
200
201
|
export type HookType = 'command' | 'http' | 'code' | 'mcp_tool'
|
|
201
202
|
|
|
@@ -226,6 +227,18 @@ export interface HookContext {
|
|
|
226
227
|
userPrompt?: string
|
|
227
228
|
configKey?: string
|
|
228
229
|
configValue?: unknown
|
|
230
|
+
/** PreInference: full conversation messages for DLP inspection. */
|
|
231
|
+
messages?: Array<{ role: string; content: string }>
|
|
232
|
+
/** PreInference: recent tool calls and their results. */
|
|
233
|
+
toolCalls?: Array<{
|
|
234
|
+
name: string
|
|
235
|
+
input: Record<string, unknown>
|
|
236
|
+
resultPreview: string
|
|
237
|
+
}>
|
|
238
|
+
/** PreInference: current provider ID. */
|
|
239
|
+
provider?: string
|
|
240
|
+
/** PreInference: current model ID. */
|
|
241
|
+
model?: string
|
|
229
242
|
}
|
|
230
243
|
|
|
231
244
|
export interface HookResult {
|
|
@@ -278,3 +291,47 @@ export interface PermissionRule {
|
|
|
278
291
|
level: PermissionLevel
|
|
279
292
|
pattern?: string
|
|
280
293
|
}
|
|
294
|
+
|
|
295
|
+
// ── Inference Hook (DLP) Types ──
|
|
296
|
+
|
|
297
|
+
/** Configuration for the PreInference DLP hook, loaded from config.yml. */
|
|
298
|
+
export interface InferenceHookConfig {
|
|
299
|
+
/** DLP server endpoint (HTTPS). Empty = feature disabled. */
|
|
300
|
+
endpoint: string
|
|
301
|
+
/** HMAC signing secret (format: mis_<random>). */
|
|
302
|
+
signing_secret: string
|
|
303
|
+
/** Request timeout in milliseconds. Default 5000. */
|
|
304
|
+
timeout: number
|
|
305
|
+
/** Failure posture: 'fail-closed' blocks on error, 'fail-open' allows. */
|
|
306
|
+
on_failure: 'fail-closed' | 'fail-open'
|
|
307
|
+
/** Organization identifier (optional, sent in payload). */
|
|
308
|
+
organization_id: string
|
|
309
|
+
/** Additional custom headers to send with each request. */
|
|
310
|
+
headers: Record<string, string>
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Outgoing request to the DLP server. */
|
|
314
|
+
export interface InferenceCheckRequest {
|
|
315
|
+
type: 'inference_check'
|
|
316
|
+
id: string
|
|
317
|
+
created_at: string
|
|
318
|
+
data: {
|
|
319
|
+
type: 'pre_inference'
|
|
320
|
+
session_id: string
|
|
321
|
+
organization_id?: string
|
|
322
|
+
provider: string
|
|
323
|
+
model: string
|
|
324
|
+
messages: Array<{ role: string; content: string }>
|
|
325
|
+
tool_calls: Array<{
|
|
326
|
+
name: string
|
|
327
|
+
input: Record<string, unknown>
|
|
328
|
+
result_preview: string
|
|
329
|
+
}>
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Response from the DLP server. */
|
|
334
|
+
export interface InferenceCheckResponse {
|
|
335
|
+
verdict: 'allow' | 'deny'
|
|
336
|
+
reason?: string
|
|
337
|
+
}
|
package/src/ui/commands.ts
CHANGED
|
@@ -3272,7 +3272,8 @@ const ideCmd: CommandHandler = async (_ctx) => {
|
|
|
3272
3272
|
// ═══════════════════════════════════════════════════════════════
|
|
3273
3273
|
|
|
3274
3274
|
const terminalSetupCmd: CommandHandler = async () => {
|
|
3275
|
-
const { writeFileSync, appendFileSync, existsSync, mkdirSync } =
|
|
3275
|
+
const { writeFileSync, appendFileSync, existsSync, mkdirSync, readFileSync } =
|
|
3276
|
+
await import('node:fs')
|
|
3276
3277
|
const { join } = await import('node:path')
|
|
3277
3278
|
const { homedir } = await import('node:os')
|
|
3278
3279
|
|
|
@@ -3313,9 +3314,7 @@ const terminalSetupCmd: CommandHandler = async () => {
|
|
|
3313
3314
|
const sourceLine = `\n# Mipham Code shell integration\n[ -f ~/.mipham/shell-setup.sh ] && source ~/.mipham/shell-setup.sh\n`
|
|
3314
3315
|
|
|
3315
3316
|
try {
|
|
3316
|
-
const existing = existsSync(profilePath)
|
|
3317
|
-
? require('node:fs').readFileSync(profilePath, 'utf-8')
|
|
3318
|
-
: ''
|
|
3317
|
+
const existing = existsSync(profilePath) ? readFileSync(profilePath, 'utf-8') : ''
|
|
3319
3318
|
if (existing.includes('shell-setup.sh')) {
|
|
3320
3319
|
lines.push(` ⏭ ${profileName} already has Mipham Code integration`)
|
|
3321
3320
|
} else {
|
package/dist/mipham
DELETED
|
Binary file
|