@inspecto-dev/cli 0.3.8 → 0.3.10

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'
@@ -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,37 @@ 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>>(
265
+ path.join(projectRoot, '.inspecto', 'settings.local.json'),
266
+ )) ?? {}
267
+ const sharedSettings =
268
+ (await readJSON<Record<string, unknown>>(
269
+ path.join(projectRoot, '.inspecto', 'settings.json'),
270
+ )) ?? {}
271
+ const annotateDeliveryMode =
272
+ (localSettings['annotate.deliveryMode'] as string | undefined) ??
273
+ (sharedSettings['annotate.deliveryMode'] as string | undefined)
274
+
275
+ if (annotateDeliveryMode !== 'agent') {
276
+ return undefined
277
+ }
278
+
279
+ return {
280
+ mode: 'agent',
281
+ skill: 'inspecto-agent',
282
+ prompt: 'Use $inspecto-agent to claim Inspecto tasks continuously',
283
+ requiresMcp: true,
284
+ }
285
+ }
286
+
259
287
  function buildPreApplyResult(
260
288
  status: ResolvedOnboardingSession['status'],
261
289
  session: ResolvedOnboardingSession,
290
+ dailyUsage?: OnboardingDailyUsageHandoff,
262
291
  ): OnboardCommandResult {
263
292
  const diagnostics: OnboardingDiagnostics | undefined =
264
293
  session.summary.risks.length > 0 ||
@@ -292,6 +321,7 @@ function buildPreApplyResult(
292
321
  ...(session.pendingSteps ? { pendingSteps: session.pendingSteps } : {}),
293
322
  ...(session.assistantPrompt ? { assistantPrompt: session.assistantPrompt } : {}),
294
323
  ...(session.patches ? { patches: session.patches } : {}),
324
+ ...(dailyUsage ? { dailyUsage } : {}),
295
325
  ...(diagnostics ? { diagnostics } : {}),
296
326
  }
297
327
  }
@@ -353,6 +383,7 @@ export async function resolveOnboardingSession(
353
383
  ): Promise<ResolvedOnboardingSession> {
354
384
  const rootContext = await buildOnboardingContext(root)
355
385
  const rootVerification = await buildVerification(root, rootContext.packageManager)
386
+ const rootDailyUsage = await buildDailyUsageHandoff(root)
356
387
  const frameworkSupportByPackage = await detectFrameworkSupportByPackage(root, rootContext)
357
388
  const target = resolveOnboardingTarget({
358
389
  repoRoot: root,
@@ -392,6 +423,7 @@ export async function resolveOnboardingSession(
392
423
  ...(plan.pendingSteps ? { pendingSteps: plan.pendingSteps } : {}),
393
424
  ...(plan.assistantPrompt ? { assistantPrompt: plan.assistantPrompt } : {}),
394
425
  ...(plan.patches ? { patches: plan.patches } : {}),
426
+ ...(rootDailyUsage ? { dailyUsage: rootDailyUsage } : {}),
395
427
  }
396
428
  }
397
429
 
@@ -418,6 +450,7 @@ export async function resolveOnboardingSession(
418
450
 
419
451
  const context = await buildTargetedContext(rootContext, target.selected!)
420
452
  const verification = await buildVerification(context.root, context.packageManager)
453
+ const dailyUsage = await buildDailyUsageHandoff(context.root)
421
454
  const plan = createPlanResult(context)
422
455
  const summary = buildOnboardingSummary(plan, context.root)
423
456
  const confirmation = buildConfirmation(
@@ -469,6 +502,7 @@ export async function resolveOnboardingSession(
469
502
  ...(plan.pendingSteps ? { pendingSteps: plan.pendingSteps } : {}),
470
503
  ...(plan.assistantPrompt ? { assistantPrompt: plan.assistantPrompt } : {}),
471
504
  ...(plan.patches ? { patches: plan.patches } : {}),
505
+ ...(dailyUsage ? { dailyUsage } : {}),
472
506
  }
473
507
  }
474
508
 
@@ -477,6 +511,7 @@ export async function applyResolvedOnboardingSession(
477
511
  options: ResolveOnboardingSessionOptions = {},
478
512
  ): Promise<OnboardCommandResult> {
479
513
  const verification = await buildVerification(session.projectRoot, session.context.packageManager)
514
+ const dailyUsage = await buildDailyUsageHandoff(session.projectRoot)
480
515
  const applyResult = await applyOnboardingPlan({
481
516
  repoRoot: process.cwd(),
482
517
  projectRoot: session.projectRoot,
@@ -528,12 +563,17 @@ export async function applyResolvedOnboardingSession(
528
563
  ...(session.pendingSteps ? { pendingSteps: session.pendingSteps } : {}),
529
564
  ...(session.assistantPrompt ? { assistantPrompt: session.assistantPrompt } : {}),
530
565
  ...(session.patches ? { patches: session.patches } : {}),
566
+ ...(dailyUsage ? { dailyUsage } : {}),
531
567
  ...(diagnostics ? { diagnostics } : {}),
532
568
  }
533
569
  }
534
570
 
535
- export function buildDeferredOnboardResult(
571
+ export async function buildDeferredOnboardResult(
536
572
  session: ResolvedOnboardingSession,
537
- ): OnboardCommandResult {
538
- return buildPreApplyResult(session.status, session)
573
+ ): Promise<OnboardCommandResult> {
574
+ return buildPreApplyResult(
575
+ session.status,
576
+ session,
577
+ await buildDailyUsageHandoff(session.projectRoot),
578
+ )
539
579
  }
@@ -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://127.0.0.1:' + ((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://127.0.0.1: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://127.0.0.1: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([