@inspecto-dev/cli 0.3.8 → 0.3.9

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/src/index.ts CHANGED
@@ -4,6 +4,12 @@ export { devLink, devStatus, devUnlink } from './commands/dev-config.js'
4
4
  export { init } from './commands/init.js'
5
5
  export { collectDoctorResult, doctor } from './commands/doctor.js'
6
6
  export { integrationDoctor } from './commands/integration-doctor.js'
7
+ export {
8
+ startMcpServer,
9
+ createInspectoMcpRuntime,
10
+ createInspectoMcpServer,
11
+ resolveInspectoServerBaseUrl,
12
+ } from './commands/mcp.js'
7
13
  export { onboard } from './commands/onboard.js'
8
14
  export { plan } from './commands/plan.js'
9
15
  export { teardown } from './commands/teardown.js'
@@ -83,7 +83,7 @@ export function printNextJsManualInstructions() {
83
83
  ' useEffect(() => {',
84
84
  " if (process.env.NODE_ENV !== 'production') {",
85
85
  " import('@inspecto-dev/core').then(({ mountInspector }) => {",
86
- " mountInspector({ serverUrl: 'http://127.0.0.1:5678' })",
86
+ " mountInspector({ serverUrl: 'http://0.0.0.0:5678' })",
87
87
  ' })',
88
88
  ' }',
89
89
  ' }, [])',
@@ -14,6 +14,7 @@ import { resolveOnboardingTarget } from './target-resolution.js'
14
14
  import { readJSON } from '../utils/fs.js'
15
15
  import type {
16
16
  OnboardCommandResult,
17
+ OnboardingDailyUsageHandoff,
17
18
  OnboardingContext,
18
19
  OnboardingDiagnostics,
19
20
  OnboardingExecutionResult,
@@ -256,9 +257,34 @@ function buildConfirmation(
256
257
  }
257
258
  }
258
259
 
260
+ async function buildDailyUsageHandoff(
261
+ projectRoot: string,
262
+ ): Promise<OnboardingDailyUsageHandoff | undefined> {
263
+ const localSettings =
264
+ (await readJSON<Record<string, unknown>>(path.join(projectRoot, '.inspecto', 'settings.local.json'))) ??
265
+ {}
266
+ const sharedSettings =
267
+ (await readJSON<Record<string, unknown>>(path.join(projectRoot, '.inspecto', 'settings.json'))) ?? {}
268
+ const annotateDeliveryMode =
269
+ (localSettings['annotate.deliveryMode'] as string | undefined) ??
270
+ (sharedSettings['annotate.deliveryMode'] as string | undefined)
271
+
272
+ if (annotateDeliveryMode !== 'agent') {
273
+ return undefined
274
+ }
275
+
276
+ return {
277
+ mode: 'agent',
278
+ skill: 'inspecto-agent',
279
+ prompt: 'Use $inspecto-agent to claim Inspecto tasks continuously',
280
+ requiresMcp: true,
281
+ }
282
+ }
283
+
259
284
  function buildPreApplyResult(
260
285
  status: ResolvedOnboardingSession['status'],
261
286
  session: ResolvedOnboardingSession,
287
+ dailyUsage?: OnboardingDailyUsageHandoff,
262
288
  ): OnboardCommandResult {
263
289
  const diagnostics: OnboardingDiagnostics | undefined =
264
290
  session.summary.risks.length > 0 ||
@@ -292,6 +318,7 @@ function buildPreApplyResult(
292
318
  ...(session.pendingSteps ? { pendingSteps: session.pendingSteps } : {}),
293
319
  ...(session.assistantPrompt ? { assistantPrompt: session.assistantPrompt } : {}),
294
320
  ...(session.patches ? { patches: session.patches } : {}),
321
+ ...(dailyUsage ? { dailyUsage } : {}),
295
322
  ...(diagnostics ? { diagnostics } : {}),
296
323
  }
297
324
  }
@@ -353,6 +380,7 @@ export async function resolveOnboardingSession(
353
380
  ): Promise<ResolvedOnboardingSession> {
354
381
  const rootContext = await buildOnboardingContext(root)
355
382
  const rootVerification = await buildVerification(root, rootContext.packageManager)
383
+ const rootDailyUsage = await buildDailyUsageHandoff(root)
356
384
  const frameworkSupportByPackage = await detectFrameworkSupportByPackage(root, rootContext)
357
385
  const target = resolveOnboardingTarget({
358
386
  repoRoot: root,
@@ -392,6 +420,7 @@ export async function resolveOnboardingSession(
392
420
  ...(plan.pendingSteps ? { pendingSteps: plan.pendingSteps } : {}),
393
421
  ...(plan.assistantPrompt ? { assistantPrompt: plan.assistantPrompt } : {}),
394
422
  ...(plan.patches ? { patches: plan.patches } : {}),
423
+ ...(rootDailyUsage ? { dailyUsage: rootDailyUsage } : {}),
395
424
  }
396
425
  }
397
426
 
@@ -418,6 +447,7 @@ export async function resolveOnboardingSession(
418
447
 
419
448
  const context = await buildTargetedContext(rootContext, target.selected!)
420
449
  const verification = await buildVerification(context.root, context.packageManager)
450
+ const dailyUsage = await buildDailyUsageHandoff(context.root)
421
451
  const plan = createPlanResult(context)
422
452
  const summary = buildOnboardingSummary(plan, context.root)
423
453
  const confirmation = buildConfirmation(
@@ -469,6 +499,7 @@ export async function resolveOnboardingSession(
469
499
  ...(plan.pendingSteps ? { pendingSteps: plan.pendingSteps } : {}),
470
500
  ...(plan.assistantPrompt ? { assistantPrompt: plan.assistantPrompt } : {}),
471
501
  ...(plan.patches ? { patches: plan.patches } : {}),
502
+ ...(dailyUsage ? { dailyUsage } : {}),
472
503
  }
473
504
  }
474
505
 
@@ -477,6 +508,7 @@ export async function applyResolvedOnboardingSession(
477
508
  options: ResolveOnboardingSessionOptions = {},
478
509
  ): Promise<OnboardCommandResult> {
479
510
  const verification = await buildVerification(session.projectRoot, session.context.packageManager)
511
+ const dailyUsage = await buildDailyUsageHandoff(session.projectRoot)
480
512
  const applyResult = await applyOnboardingPlan({
481
513
  repoRoot: process.cwd(),
482
514
  projectRoot: session.projectRoot,
@@ -528,12 +560,17 @@ export async function applyResolvedOnboardingSession(
528
560
  ...(session.pendingSteps ? { pendingSteps: session.pendingSteps } : {}),
529
561
  ...(session.assistantPrompt ? { assistantPrompt: session.assistantPrompt } : {}),
530
562
  ...(session.patches ? { patches: session.patches } : {}),
563
+ ...(dailyUsage ? { dailyUsage } : {}),
531
564
  ...(diagnostics ? { diagnostics } : {}),
532
565
  }
533
566
  }
534
567
 
535
- export function buildDeferredOnboardResult(
568
+ export async function buildDeferredOnboardResult(
536
569
  session: ResolvedOnboardingSession,
537
- ): OnboardCommandResult {
538
- return buildPreApplyResult(session.status, session)
570
+ ): Promise<OnboardCommandResult> {
571
+ return buildPreApplyResult(
572
+ session.status,
573
+ session,
574
+ await buildDailyUsageHandoff(session.projectRoot),
575
+ )
539
576
  }
@@ -93,7 +93,7 @@ function buildUmiMountSnippet(): string {
93
93
  " if (process.env.NODE_ENV !== 'production') {",
94
94
  " import('@inspecto-dev/core').then(({ mountInspector }) => {",
95
95
  ' mountInspector({',
96
- " serverUrl: 'http://127.0.0.1:' + ((window as any).__AI_INSPECTOR_PORT__ || 5678),",
96
+ " serverUrl: 'http://0.0.0.0:' + ((window as { __AI_INSPECTOR_PORT__?: number }).__AI_INSPECTOR_PORT__ || 5678),",
97
97
  ' })',
98
98
  ' })',
99
99
  ' }',
package/src/prompts.ts CHANGED
@@ -10,23 +10,33 @@ export async function promptIDEChoice(
10
10
  detections: { ide: string; supported: boolean }[],
11
11
  ): Promise<{ ide: string; supported: boolean } | null> {
12
12
  if (!process.stdin.isTTY) {
13
- log.warn('Multiple IDEs detected but stdin is not interactive')
14
- log.hint(`Using: ${detections[0]!.ide} (first match)`)
15
- return detections[0]!
13
+ if (detections.length > 0) {
14
+ log.warn('Multiple IDEs detected but stdin is not interactive')
15
+ log.hint(`Using: ${detections[0]!.ide} (first match)`)
16
+ return detections[0]!
17
+ }
18
+ return { ide: 'none', supported: true }
16
19
  }
17
20
 
21
+ const choices = detections.map(d => ({
22
+ title: `${d.ide} ${d.supported ? '(supported)' : '(unsupported/limited)'}`,
23
+ value: d,
24
+ }))
25
+
26
+ choices.push({
27
+ title: 'none (Standalone / MCP / Browser-only)',
28
+ value: { ide: 'none', supported: true },
29
+ })
30
+
18
31
  const { choice } = await prompts({
19
32
  type: 'select',
20
33
  name: 'choice',
21
- message: 'Detected multiple IDEs, please choose one:',
22
- choices: detections.map((d, i) => ({
23
- title: `${d.ide} ${d.supported ? '(supported)' : '(unsupported/limited)'}`,
24
- value: i,
25
- })),
34
+ message: 'Detected multiple IDEs, please choose one (or select none for standalone use):',
35
+ choices,
26
36
  })
27
37
 
28
38
  if (choice === undefined) return null
29
- return detections[choice]!
39
+ return choice
30
40
  }
31
41
 
32
42
  /**
package/src/types.ts CHANGED
@@ -134,6 +134,13 @@ export interface OnboardingVerification {
134
134
  message: string
135
135
  }
136
136
 
137
+ export interface OnboardingDailyUsageHandoff {
138
+ mode: 'agent'
139
+ skill: string
140
+ prompt: string
141
+ requiresMcp: boolean
142
+ }
143
+
137
144
  export interface OnboardingAssistantHandoff {
138
145
  framework?: string
139
146
  metaFramework?: string
@@ -142,6 +149,7 @@ export interface OnboardingAssistantHandoff {
142
149
  pendingSteps?: string[]
143
150
  assistantPrompt?: string
144
151
  patches?: OnboardingPatchPlan[]
152
+ dailyUsage?: OnboardingDailyUsageHandoff
145
153
  }
146
154
 
147
155
  export interface ResolvedOnboardingSession {
@@ -162,6 +170,7 @@ export interface ResolvedOnboardingSession {
162
170
  pendingSteps?: string[]
163
171
  assistantPrompt?: string
164
172
  patches?: OnboardingPatchPlan[]
173
+ dailyUsage?: OnboardingDailyUsageHandoff
165
174
  handoff?: OnboardingAssistantHandoff
166
175
  }
167
176
 
@@ -181,6 +190,7 @@ export interface OnboardCommandResult {
181
190
  pendingSteps?: string[]
182
191
  assistantPrompt?: string
183
192
  patches?: OnboardingPatchPlan[]
193
+ dailyUsage?: OnboardingDailyUsageHandoff
184
194
  handoff?: OnboardingAssistantHandoff
185
195
  }
186
196
 
@@ -126,6 +126,14 @@ describe('integration install', () => {
126
126
  '/Users/tester/.agents/skills/inspecto-onboarding-codex/scripts/run-inspecto.sh',
127
127
  '# mock asset',
128
128
  )
129
+ expect(writeFileMock).toHaveBeenCalledWith(
130
+ '/Users/tester/.agents/skills/inspecto-agent-codex/SKILL.md',
131
+ '# mock asset',
132
+ )
133
+ expect(writeFileMock).toHaveBeenCalledWith(
134
+ '/Users/tester/.agents/skills/inspecto-agent-codex/agents/openai.yaml',
135
+ '# mock asset',
136
+ )
129
137
  expect(chmodMock).toHaveBeenCalledWith(
130
138
  '/Users/tester/.agents/skills/inspecto-onboarding-codex/scripts/run-inspecto.sh',
131
139
  0o755,
@@ -134,6 +142,9 @@ describe('integration install', () => {
134
142
  expect(logMock.hint).toHaveBeenCalledWith(
135
143
  '/Users/tester/.agents/skills/inspecto-onboarding-codex/SKILL.md',
136
144
  )
145
+ expect(logMock.hint).toHaveBeenCalledWith(
146
+ '/Users/tester/.agents/skills/inspecto-agent-codex/SKILL.md',
147
+ )
137
148
  expect(runIntegrationAutomationMock).not.toHaveBeenCalled()
138
149
  expect(logMock.ready).toHaveBeenCalledWith(
139
150
  'Installed Codex integration assets. User-level installs only write integration assets and do not launch onboarding automatically.',
@@ -199,6 +210,7 @@ describe('integration install', () => {
199
210
  expect(writeJSONMock).toHaveBeenCalledWith('/repo/.inspecto/settings.local.json', {
200
211
  ide: 'trae-cn',
201
212
  'provider.default': 'gemini.extension',
213
+ 'annotate.deliveryMode': 'both',
202
214
  })
203
215
  })
204
216
 
@@ -216,6 +228,7 @@ describe('integration install', () => {
216
228
  expect(writeJSONMock).toHaveBeenCalledWith('/repo/.inspecto/settings.local.json', {
217
229
  ide: 'trae-cn',
218
230
  'provider.default': 'gemini.extension',
231
+ 'annotate.deliveryMode': 'both',
219
232
  'prompt.autoSend': true,
220
233
  })
221
234
  })
@@ -228,6 +241,7 @@ describe('integration install', () => {
228
241
  expect(writeJSONMock).toHaveBeenCalledWith('/repo/.inspecto/settings.local.json', {
229
242
  ide: 'cursor',
230
243
  'provider.default': 'codex.extension',
244
+ 'annotate.deliveryMode': 'both',
231
245
  })
232
246
  })
233
247
 
@@ -540,6 +554,8 @@ describe('integration install', () => {
540
554
  '/Users/tester/.agents/skills/inspecto-onboarding-codex/SKILL.md',
541
555
  '/Users/tester/.agents/skills/inspecto-onboarding-codex/agents/openai.yaml',
542
556
  '/Users/tester/.agents/skills/inspecto-onboarding-codex/scripts/run-inspecto.sh',
557
+ '/Users/tester/.agents/skills/inspecto-agent-codex/SKILL.md',
558
+ '/Users/tester/.agents/skills/inspecto-agent-codex/agents/openai.yaml',
543
559
  ],
544
560
  preferredInstall:
545
561
  'npx @inspecto-dev/cli integrations install codex --host-ide <vscode|cursor|trae|trae-cn|codebuddy|codebuddy-cn>',
@@ -0,0 +1,197 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import crypto from 'node:crypto'
6
+ import {
7
+ INSPECTO_MCP_TOOLS,
8
+ createInspectoMcpRuntime,
9
+ resolveInspectoServerBaseUrl,
10
+ } from '../src/commands/mcp.js'
11
+
12
+ describe('inspecto mcp command', () => {
13
+ const portFile = path.join(os.tmpdir(), 'inspecto.port.json')
14
+ let originalPortFile: string | null = null
15
+
16
+ afterEach(() => {
17
+ vi.restoreAllMocks()
18
+ try {
19
+ fs.unlinkSync(portFile)
20
+ } catch {
21
+ // ignore
22
+ }
23
+ if (originalPortFile !== null) {
24
+ fs.writeFileSync(portFile, originalPortFile, 'utf-8')
25
+ originalPortFile = null
26
+ }
27
+ })
28
+
29
+ it('resolves the current project server URL from inspecto.port.json', () => {
30
+ originalPortFile = fs.existsSync(portFile) ? fs.readFileSync(portFile, 'utf-8') : null
31
+ const cwd = '/repo/current'
32
+ const currentHash = crypto.createHash('md5').update(cwd).digest('hex')
33
+ const otherHash = crypto.createHash('md5').update('/repo/other').digest('hex')
34
+
35
+ fs.writeFileSync(
36
+ portFile,
37
+ JSON.stringify(
38
+ {
39
+ [otherHash]: 5679,
40
+ [currentHash]: 5680,
41
+ },
42
+ null,
43
+ 2,
44
+ ),
45
+ 'utf-8',
46
+ )
47
+
48
+ expect(resolveInspectoServerBaseUrl(cwd)).toBe('http://0.0.0.0:5680')
49
+ })
50
+
51
+ it('resolves the project server URL when Codex runs from a nested working directory', () => {
52
+ originalPortFile = fs.existsSync(portFile) ? fs.readFileSync(portFile, 'utf-8') : null
53
+ const projectRoot = '/repo/current'
54
+ const nestedCwd = '/repo/current/packages/app'
55
+ const projectRootHash = crypto.createHash('md5').update(projectRoot).digest('hex')
56
+
57
+ fs.writeFileSync(
58
+ portFile,
59
+ JSON.stringify(
60
+ {
61
+ [projectRootHash]: 5681,
62
+ },
63
+ null,
64
+ 2,
65
+ ),
66
+ 'utf-8',
67
+ )
68
+
69
+ expect(resolveInspectoServerBaseUrl(nestedCwd)).toBe('http://0.0.0.0:5681')
70
+ })
71
+
72
+ it('exposes the expected MCP tool definitions', () => {
73
+ expect(INSPECTO_MCP_TOOLS.map(tool => tool.name)).toEqual([
74
+ 'inspecto_get_session',
75
+ 'inspecto_claim_next',
76
+ 'inspecto_reply',
77
+ 'inspecto_resolve',
78
+ 'inspecto_dismiss',
79
+ ])
80
+ })
81
+
82
+ it('maps getSession and claimNext to the HTTP session endpoints', async () => {
83
+ const fetchMock = vi
84
+ .spyOn(globalThis, 'fetch')
85
+ .mockResolvedValueOnce({
86
+ ok: true,
87
+ json: async () => ({ success: true, session: { id: 'session-1' } }),
88
+ } as Response)
89
+ .mockResolvedValueOnce({
90
+ ok: true,
91
+ json: async () => ({
92
+ success: true,
93
+ timedOut: false,
94
+ matchedExisting: true,
95
+ session: { id: 'session-claim', status: 'acknowledged' },
96
+ }),
97
+ } as Response)
98
+ const runtime = createInspectoMcpRuntime('http://0.0.0.0:5678')
99
+
100
+ const session = await runtime.getSession({ sessionId: 'session-1' })
101
+
102
+ expect(fetchMock).toHaveBeenCalledWith('http://0.0.0.0:5678/inspecto/api/v1/sessions/session-1')
103
+ expect(session).toEqual({ success: true, session: { id: 'session-1' } })
104
+
105
+ const claim = await runtime.claimNext()
106
+
107
+ expect(fetchMock).toHaveBeenCalledWith(
108
+ 'http://0.0.0.0:5678/inspecto/api/v1/sessions/claim',
109
+ expect.objectContaining({
110
+ method: 'POST',
111
+ body: JSON.stringify({}),
112
+ }),
113
+ )
114
+ expect(claim).toEqual({
115
+ success: true,
116
+ timedOut: false,
117
+ matchedExisting: true,
118
+ session: { id: 'session-claim', status: 'acknowledged' },
119
+ })
120
+ })
121
+
122
+ it('maps reply, resolve, and dismiss to session mutation endpoints', async () => {
123
+ const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
124
+ ok: true,
125
+ json: async () => ({ success: true, session: { id: 'session-1' } }),
126
+ } as Response)
127
+ const runtime = createInspectoMcpRuntime('http://0.0.0.0:5678')
128
+
129
+ const reply = await runtime.reply({
130
+ sessionId: 'session-1',
131
+ text: 'Working on it.',
132
+ })
133
+
134
+ expect(fetchMock).toHaveBeenCalledWith(
135
+ 'http://0.0.0.0:5678/inspecto/api/v1/sessions/session-1/reply',
136
+ expect.objectContaining({
137
+ method: 'POST',
138
+ }),
139
+ )
140
+ expect(reply).toEqual({ success: true, session: { id: 'session-1' } })
141
+
142
+ const resolve = await runtime.resolve({
143
+ sessionId: 'session-1',
144
+ message: 'Done.',
145
+ })
146
+
147
+ expect(fetchMock).toHaveBeenCalledWith(
148
+ 'http://0.0.0.0:5678/inspecto/api/v1/sessions/session-1/resolve',
149
+ expect.objectContaining({
150
+ method: 'POST',
151
+ }),
152
+ )
153
+ expect(resolve).toEqual({ success: true, session: { id: 'session-1' } })
154
+
155
+ const dismiss = await runtime.dismiss({
156
+ sessionId: 'session-1',
157
+ message: 'No action needed.',
158
+ })
159
+
160
+ expect(fetchMock).toHaveBeenCalledWith(
161
+ 'http://0.0.0.0:5678/inspecto/api/v1/sessions/session-1/dismiss',
162
+ expect.objectContaining({
163
+ method: 'POST',
164
+ }),
165
+ )
166
+ expect(dismiss).toEqual({ success: true, session: { id: 'session-1' } })
167
+ })
168
+
169
+ it('surfaces failed session mutations as rejected runtime calls', async () => {
170
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue({
171
+ ok: false,
172
+ status: 404,
173
+ json: async () => ({ success: false, error: 'Session not found' }),
174
+ } as Response)
175
+ const runtime = createInspectoMcpRuntime('http://0.0.0.0:5678')
176
+
177
+ await expect(
178
+ runtime.reply({
179
+ sessionId: 'missing-session',
180
+ text: 'Working on it.',
181
+ }),
182
+ ).rejects.toThrow('Session not found')
183
+ })
184
+
185
+ it('surfaces direct session lookup failures as rejected runtime calls', async () => {
186
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue({
187
+ ok: false,
188
+ status: 404,
189
+ json: async () => ({ success: false, error: 'Session not found' }),
190
+ } as Response)
191
+ const runtime = createInspectoMcpRuntime('http://0.0.0.0:5678')
192
+
193
+ await expect(runtime.getSession({ sessionId: 'missing-session' })).rejects.toThrow(
194
+ 'Session not found',
195
+ )
196
+ })
197
+ })
@@ -352,6 +352,12 @@ describe('onboard command', () => {
352
352
  'Complete the remaining client-side mount step for your App Router entry.',
353
353
  ],
354
354
  assistantPrompt: 'Complete the remaining Inspecto onboarding for this Next.js project.',
355
+ dailyUsage: {
356
+ mode: 'agent',
357
+ skill: 'inspecto-agent',
358
+ prompt: 'Use $inspecto-agent to claim Inspecto tasks continuously',
359
+ requiresMcp: true,
360
+ },
355
361
  patches: [
356
362
  {
357
363
  path: 'next.config.mjs',
@@ -393,6 +399,12 @@ describe('onboard command', () => {
393
399
  'Complete the remaining client-side mount step for your App Router entry.',
394
400
  ],
395
401
  assistantPrompt: 'Complete the remaining Inspecto onboarding for this Next.js project.',
402
+ dailyUsage: {
403
+ mode: 'agent',
404
+ skill: 'inspecto-agent',
405
+ prompt: 'Use $inspecto-agent to claim Inspecto tasks continuously',
406
+ requiresMcp: true,
407
+ },
396
408
  patches: [
397
409
  {
398
410
  path: 'next.config.mjs',
@@ -425,6 +437,9 @@ describe('onboard command', () => {
425
437
  'Complete the remaining client-side mount step for your App Router entry.',
426
438
  ]),
427
439
  )
440
+ expect(result.handoff?.dailyUsage?.prompt).toBe(
441
+ 'Use $inspecto-agent to claim Inspecto tasks continuously',
442
+ )
428
443
  expect(result.assistantPrompt).toContain('Complete the remaining Inspecto onboarding')
429
444
  expect(result.patches).toEqual(
430
445
  expect.arrayContaining([