@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/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+
1
3
  /** Package manager detection result */
2
4
  type PackageManager = 'bun' | 'pnpm' | 'yarn' | 'npm';
3
5
  /** Supported build tools (v1) */
@@ -111,6 +113,12 @@ interface OnboardingVerification {
111
113
  devCommand?: string;
112
114
  message: string;
113
115
  }
116
+ interface OnboardingDailyUsageHandoff {
117
+ mode: 'agent';
118
+ skill: string;
119
+ prompt: string;
120
+ requiresMcp: boolean;
121
+ }
114
122
  interface OnboardingAssistantHandoff {
115
123
  framework?: string;
116
124
  metaFramework?: string;
@@ -119,6 +127,7 @@ interface OnboardingAssistantHandoff {
119
127
  pendingSteps?: string[];
120
128
  assistantPrompt?: string;
121
129
  patches?: OnboardingPatchPlan[];
130
+ dailyUsage?: OnboardingDailyUsageHandoff;
122
131
  }
123
132
  interface ResolvedOnboardingSession {
124
133
  status: OnboardStatus;
@@ -141,6 +150,7 @@ interface ResolvedOnboardingSession {
141
150
  pendingSteps?: string[];
142
151
  assistantPrompt?: string;
143
152
  patches?: OnboardingPatchPlan[];
153
+ dailyUsage?: OnboardingDailyUsageHandoff;
144
154
  handoff?: OnboardingAssistantHandoff;
145
155
  }
146
156
  interface OnboardCommandResult {
@@ -159,6 +169,7 @@ interface OnboardCommandResult {
159
169
  pendingSteps?: string[];
160
170
  assistantPrompt?: string;
161
171
  patches?: OnboardingPatchPlan[];
172
+ dailyUsage?: OnboardingDailyUsageHandoff;
162
173
  handoff?: OnboardingAssistantHandoff;
163
174
  }
164
175
  /** Machine-readable detection output for skill-first onboarding */
@@ -383,6 +394,44 @@ interface IntegrationDoctorResult {
383
394
  }
384
395
  declare function integrationDoctor(assistant: string, options?: IntegrationDoctorOptions): Promise<IntegrationDoctorResult>;
385
396
 
397
+ interface McpCommandOptions {
398
+ serverUrl?: string;
399
+ version?: string;
400
+ }
401
+ interface InspectoMcpRuntime {
402
+ getSession(args: {
403
+ sessionId: string;
404
+ }): Promise<Record<string, unknown>>;
405
+ claimNext(args?: {
406
+ timeoutMs?: number;
407
+ }): Promise<{
408
+ success: boolean;
409
+ timedOut: boolean;
410
+ matchedExisting: boolean;
411
+ session?: Record<string, unknown>;
412
+ event?: string;
413
+ }>;
414
+ reply(args: {
415
+ sessionId: string;
416
+ text: string;
417
+ }): Promise<Record<string, unknown>>;
418
+ resolve(args: {
419
+ sessionId: string;
420
+ message?: string;
421
+ }): Promise<Record<string, unknown>>;
422
+ dismiss(args: {
423
+ sessionId: string;
424
+ message?: string;
425
+ }): Promise<Record<string, unknown>>;
426
+ }
427
+ declare function startMcpServer(options?: McpCommandOptions): Promise<void>;
428
+ declare function createInspectoMcpServer(options: {
429
+ baseUrl: string;
430
+ version?: string;
431
+ }): McpServer;
432
+ declare function createInspectoMcpRuntime(baseUrl: string): InspectoMcpRuntime;
433
+ declare function resolveInspectoServerBaseUrl(cwd: string): string | null;
434
+
386
435
  interface OnboardCommandOptions {
387
436
  json?: boolean;
388
437
  target?: string;
@@ -407,4 +456,4 @@ declare function reportCommandError(error: unknown, options?: ReportCommandError
407
456
 
408
457
  type Framework = 'react' | 'vue' | 'svelte' | 'solid' | 'astro';
409
458
 
410
- export { type BuildTool, type DoctorDiagnostic, type DoctorResult, type Framework, type InitOptions, type InstallLock, type OnboardCommandResult, type OnboardStatus, type PackageManager, type ResolvedOnboardingSession, apply, collectDoctorResult, detect, devLink, devStatus, devUnlink, doctor, init, integrationDoctor, onboard, plan, reportCommandError, teardown, writeCommandOutput };
459
+ export { type BuildTool, type DoctorDiagnostic, type DoctorResult, type Framework, type InitOptions, type InstallLock, type OnboardCommandResult, type OnboardStatus, type PackageManager, type ResolvedOnboardingSession, apply, collectDoctorResult, createInspectoMcpRuntime, createInspectoMcpServer, detect, devLink, devStatus, devUnlink, doctor, init, integrationDoctor, onboard, plan, reportCommandError, resolveInspectoServerBaseUrl, startMcpServer, teardown, writeCommandOutput };
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import {
2
2
  apply,
3
3
  collectDoctorResult,
4
+ createInspectoMcpRuntime,
5
+ createInspectoMcpServer,
4
6
  detect,
5
7
  devLink,
6
8
  devStatus,
@@ -11,12 +13,16 @@ import {
11
13
  onboard,
12
14
  plan,
13
15
  reportCommandError,
16
+ resolveInspectoServerBaseUrl,
17
+ startMcpServer,
14
18
  teardown,
15
19
  writeCommandOutput
16
- } from "./chunk-B5JDHSP7.js";
20
+ } from "./chunk-WOZPOTWE.js";
17
21
  export {
18
22
  apply,
19
23
  collectDoctorResult,
24
+ createInspectoMcpRuntime,
25
+ createInspectoMcpServer,
20
26
  detect,
21
27
  devLink,
22
28
  devStatus,
@@ -27,6 +33,8 @@ export {
27
33
  onboard,
28
34
  plan,
29
35
  reportCommandError,
36
+ resolveInspectoServerBaseUrl,
37
+ startMcpServer,
30
38
  teardown,
31
39
  writeCommandOutput
32
40
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inspecto-dev/cli",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "description": "CLI tools for Inspecto onboarding and lifecycle management",
5
5
  "keywords": [
6
6
  "inspecto",
@@ -15,12 +15,14 @@
15
15
  "inspecto": "./bin/inspecto.js"
16
16
  },
17
17
  "dependencies": {
18
+ "@modelcontextprotocol/sdk": "^1.29.0",
18
19
  "cac": "^6.7.14",
19
20
  "magicast": "^0.5.2",
20
- "ora": "^9.3.0",
21
+ "ora": "^9.4.0",
21
22
  "picocolors": "^1.0.0",
22
23
  "prompts": "^2.4.2",
23
- "@inspecto-dev/types": "0.3.8"
24
+ "zod": "^4.1.12",
25
+ "@inspecto-dev/types": "0.3.10"
24
26
  },
25
27
  "devDependencies": {
26
28
  "@types/node": "^20.19.39",
package/src/bin.ts CHANGED
@@ -11,6 +11,7 @@ import { detect } from './commands/detect.js'
11
11
  import { devLink, devStatus, devUnlink } from './commands/dev-config.js'
12
12
  import { init } from './commands/init.js'
13
13
  import { doctor } from './commands/doctor.js'
14
+ import { startMcpServer } from './commands/mcp.js'
14
15
  import { onboard } from './commands/onboard.js'
15
16
  import { plan } from './commands/plan.js'
16
17
  import { teardown } from './commands/teardown.js'
@@ -75,6 +76,10 @@ interface DevCommandOptions extends JsonCommandOptions {
75
76
  repo?: string
76
77
  }
77
78
 
79
+ interface McpCommandOptions extends GlobalOptions {
80
+ serverUrl?: string
81
+ }
82
+
78
83
  const integrationScopes = ['project', 'user'] as const
79
84
  const integrationModes = ['skills', 'instructions', 'agents', 'rules'] as const
80
85
 
@@ -128,6 +133,21 @@ export function createCli(_argv: readonly string[] = process.argv): CAC {
128
133
  }
129
134
  })
130
135
 
136
+ cli
137
+ .command('mcp', 'Run Inspecto as a minimal stdio MCP server for agents')
138
+ .option('--server-url <url>', 'Use an explicit Inspecto dev server base URL')
139
+ .option('--debug', 'Enable debug mode to show full error traces', { default: false })
140
+ .action(async (options: McpCommandOptions) => {
141
+ try {
142
+ await startMcpServer({
143
+ ...(options.serverUrl ? { serverUrl: options.serverUrl } : {}),
144
+ version,
145
+ })
146
+ } catch (error) {
147
+ exitWithError(error, options)
148
+ }
149
+ })
150
+
131
151
  cli
132
152
  .command('init', 'Set up Inspecto in your project')
133
153
  .option('--shared', 'Share .inspecto/settings.json with your team via Git', { default: false })
@@ -202,11 +202,15 @@ export async function init(options: InitOptions): Promise<void> {
202
202
  }
203
203
 
204
204
  // IDE detection
205
- let selectedIDE: { ide: string; supported: boolean } | null = null
205
+ let selectedIDE: { ide: string; supported: boolean } | null
206
206
 
207
207
  if (ideProbe.detected.length === 0) {
208
- log.error('No IDE detected in current project')
209
- log.hint('Please open this project in a supported IDE (like VS Code)')
208
+ if (process.stdin.isTTY) {
209
+ log.warn('No IDE detected in current project')
210
+ selectedIDE = await promptIDEChoice([])
211
+ } else {
212
+ selectedIDE = { ide: 'none', supported: true }
213
+ }
210
214
  } else if (ideProbe.detected.length === 1) {
211
215
  selectedIDE = ideProbe.detected[0]!
212
216
  } else {
@@ -75,6 +75,7 @@ interface InstallPlan {
75
75
  interface InspectoSettingsShape {
76
76
  ide?: string
77
77
  'provider.default'?: string
78
+ 'annotate.deliveryMode'?: 'ide' | 'agent' | 'both'
78
79
  [key: string]: unknown
79
80
  }
80
81
 
@@ -376,21 +377,31 @@ async function persistProjectOnboardingDefaults(
376
377
  resolvedHostIde.ide && resolvedHostIde.confidence !== 'low'
377
378
  ? await resolveProviderDefaultForAssistant(assistant, resolvedHostIde.ide)
378
379
  : undefined
380
+ const annotateDeliveryMode = resolveAnnotateDefaultDeliveryForAssistant(assistant)
379
381
  const mergedSettings =
380
382
  existingSettings && typeof existingSettings === 'object'
381
383
  ? {
382
384
  ...existingSettings,
383
385
  ide: options.ide,
384
386
  ...(providerDefault ? { 'provider.default': providerDefault } : {}),
387
+ 'annotate.deliveryMode': annotateDeliveryMode,
385
388
  }
386
389
  : {
387
390
  ide: options.ide,
388
391
  ...(providerDefault ? { 'provider.default': providerDefault } : {}),
392
+ 'annotate.deliveryMode': annotateDeliveryMode,
389
393
  }
390
394
 
391
395
  await writeJSON(settingsPath, mergedSettings)
392
396
  }
393
397
 
398
+ function resolveAnnotateDefaultDeliveryForAssistant(
399
+ assistant: AssistantId,
400
+ ): 'ide' | 'agent' | 'both' {
401
+ void assistant
402
+ return 'both'
403
+ }
404
+
394
405
  function shouldSkipAutomationForInstall(options: InstallIntegrationOptions): boolean {
395
406
  return options.scope === 'user' && !options.preview
396
407
  }
@@ -576,6 +587,10 @@ function resolveCodexPlan(options: InstallIntegrationOptions): InstallPlan {
576
587
  scope === 'user'
577
588
  ? path.join(homedir(), '.agents/skills/inspecto-onboarding-codex')
578
589
  : '.agents/skills/inspecto-onboarding-codex'
590
+ const agentDir =
591
+ scope === 'user'
592
+ ? path.join(homedir(), '.agents/skills/inspecto-agent-codex')
593
+ : '.agents/skills/inspecto-agent-codex'
579
594
 
580
595
  return {
581
596
  assets: [
@@ -595,9 +610,19 @@ function resolveCodexPlan(options: InstallIntegrationOptions): InstallPlan {
595
610
  executable: true,
596
611
  localSource: 'skills/inspecto-onboarding-codex/scripts/run-inspecto.sh',
597
612
  },
613
+ {
614
+ source: `${REPO_RAW_BASE}/skills/inspecto-agent-codex/SKILL.md`,
615
+ target: path.join(agentDir, 'SKILL.md'),
616
+ localSource: 'skills/inspecto-agent-codex/SKILL.md',
617
+ },
618
+ {
619
+ source: `${REPO_RAW_BASE}/skills/inspecto-agent-codex/agents/openai.yaml`,
620
+ target: path.join(agentDir, 'agents/openai.yaml'),
621
+ localSource: 'skills/inspecto-agent-codex/agents/openai.yaml',
622
+ },
598
623
  ],
599
- successMessage: `Installed Codex skill to ${baseDir}`,
600
- nextStep: 'Restart Codex or start a new Codex session to load the skill.',
624
+ successMessage: `Installed Codex skills to ${baseDir} and ${agentDir}`,
625
+ nextStep: 'Restart Codex or start a new Codex session to load the new skills.',
601
626
  }
602
627
  }
603
628
 
@@ -0,0 +1,386 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import crypto from 'node:crypto'
5
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
6
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
7
+ import { INSPECTO_API_PATHS } from '@inspecto-dev/types'
8
+ import { z } from 'zod'
9
+
10
+ const DEFAULT_MCP_SERVER_VERSION = '0.0.0'
11
+
12
+ export interface McpCommandOptions {
13
+ serverUrl?: string
14
+ version?: string
15
+ }
16
+
17
+ interface McpToolResult<T extends Record<string, unknown>> {
18
+ [key: string]: unknown
19
+ content: Array<{ type: 'text'; text: string }>
20
+ structuredContent: T
21
+ isError?: boolean
22
+ }
23
+
24
+ export interface InspectoMcpRuntime {
25
+ getSession(args: { sessionId: string }): Promise<Record<string, unknown>>
26
+ claimNext(args?: { timeoutMs?: number }): Promise<{
27
+ success: boolean
28
+ timedOut: boolean
29
+ matchedExisting: boolean
30
+ session?: Record<string, unknown>
31
+ event?: string
32
+ }>
33
+ reply(args: { sessionId: string; text: string }): Promise<Record<string, unknown>>
34
+ resolve(args: { sessionId: string; message?: string }): Promise<Record<string, unknown>>
35
+ dismiss(args: { sessionId: string; message?: string }): Promise<Record<string, unknown>>
36
+ }
37
+
38
+ export interface InspectoMcpToolDefinition {
39
+ name:
40
+ | 'inspecto_get_session'
41
+ | 'inspecto_claim_next'
42
+ | 'inspecto_reply'
43
+ | 'inspecto_resolve'
44
+ | 'inspecto_dismiss'
45
+ description: string
46
+ }
47
+
48
+ export const INSPECTO_MCP_TOOLS: InspectoMcpToolDefinition[] = [
49
+ {
50
+ name: 'inspecto_get_session',
51
+ description: 'Return one Inspecto annotation session by sessionId.',
52
+ },
53
+ {
54
+ name: 'inspecto_claim_next',
55
+ description:
56
+ 'Wait for the next unclaimed Inspecto annotation session, mark it acknowledged, and return full context.',
57
+ },
58
+ {
59
+ name: 'inspecto_reply',
60
+ description: 'Append an agent reply to an Inspecto annotation session.',
61
+ },
62
+ {
63
+ name: 'inspecto_resolve',
64
+ description: 'Resolve an Inspecto annotation session with an optional final message.',
65
+ },
66
+ {
67
+ name: 'inspecto_dismiss',
68
+ description: 'Dismiss an Inspecto annotation session with an optional final message.',
69
+ },
70
+ ]
71
+
72
+ export async function startMcpServer(options: McpCommandOptions = {}): Promise<void> {
73
+ const baseUrl = options.serverUrl ?? resolveInspectoServerBaseUrl(process.cwd())
74
+ if (!baseUrl) {
75
+ throw new Error(
76
+ 'Could not find a running Inspecto dev server. Start your local dev server or pass --server-url <url>.',
77
+ )
78
+ }
79
+
80
+ const server = createInspectoMcpServer({
81
+ baseUrl,
82
+ ...(options.version ? { version: options.version } : {}),
83
+ })
84
+ const transport = new StdioServerTransport()
85
+ await server.connect(transport)
86
+ }
87
+
88
+ export function createInspectoMcpServer(options: { baseUrl: string; version?: string }): McpServer {
89
+ const runtime = createInspectoMcpRuntime(options.baseUrl)
90
+ const server = new McpServer({
91
+ name: 'inspecto-mcp',
92
+ version: options.version ?? DEFAULT_MCP_SERVER_VERSION,
93
+ })
94
+
95
+ server.registerTool(
96
+ 'inspecto_get_session',
97
+ {
98
+ description: getToolDescription('inspecto_get_session'),
99
+ inputSchema: {
100
+ sessionId: z.string().min(1),
101
+ },
102
+ },
103
+ async ({ sessionId }) => {
104
+ try {
105
+ const result = await runtime.getSession({ sessionId })
106
+ return toolSuccess(result)
107
+ } catch (error) {
108
+ return toolError(error)
109
+ }
110
+ },
111
+ )
112
+
113
+ server.registerTool(
114
+ 'inspecto_claim_next',
115
+ {
116
+ description: getToolDescription('inspecto_claim_next'),
117
+ inputSchema: {
118
+ timeoutMs: z.number().int().nonnegative().optional(),
119
+ },
120
+ },
121
+ async ({ timeoutMs }) => {
122
+ try {
123
+ const result = await runtime.claimNext({
124
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
125
+ })
126
+ return toolSuccess(result)
127
+ } catch (error) {
128
+ return toolError(error)
129
+ }
130
+ },
131
+ )
132
+
133
+ server.registerTool(
134
+ 'inspecto_reply',
135
+ {
136
+ description: getToolDescription('inspecto_reply'),
137
+ inputSchema: {
138
+ sessionId: z.string().min(1),
139
+ text: z.string().min(1),
140
+ },
141
+ },
142
+ async ({ sessionId, text }) => {
143
+ try {
144
+ const result = await runtime.reply({ sessionId, text })
145
+ return toolSuccess(result)
146
+ } catch (error) {
147
+ return toolError(error)
148
+ }
149
+ },
150
+ )
151
+
152
+ server.registerTool(
153
+ 'inspecto_resolve',
154
+ {
155
+ description: getToolDescription('inspecto_resolve'),
156
+ inputSchema: {
157
+ sessionId: z.string().min(1),
158
+ message: z.string().optional(),
159
+ },
160
+ },
161
+ async ({ sessionId, message }) => {
162
+ try {
163
+ const result = await runtime.resolve({ sessionId, ...(message ? { message } : {}) })
164
+ return toolSuccess(result)
165
+ } catch (error) {
166
+ return toolError(error)
167
+ }
168
+ },
169
+ )
170
+
171
+ server.registerTool(
172
+ 'inspecto_dismiss',
173
+ {
174
+ description: getToolDescription('inspecto_dismiss'),
175
+ inputSchema: {
176
+ sessionId: z.string().min(1),
177
+ message: z.string().optional(),
178
+ },
179
+ },
180
+ async ({ sessionId, message }) => {
181
+ try {
182
+ const result = await runtime.dismiss({ sessionId, ...(message ? { message } : {}) })
183
+ return toolSuccess(result)
184
+ } catch (error) {
185
+ return toolError(error)
186
+ }
187
+ },
188
+ )
189
+
190
+ return server
191
+ }
192
+
193
+ export function getToolDescription(name: InspectoMcpToolDefinition['name']): string {
194
+ return INSPECTO_MCP_TOOLS.find(tool => tool.name === name)?.description ?? name
195
+ }
196
+
197
+ export function createInspectoMcpRuntime(baseUrl: string): InspectoMcpRuntime {
198
+ return {
199
+ async getSession(args) {
200
+ return getSession(baseUrl, args.sessionId)
201
+ },
202
+
203
+ async claimNext(args = {}) {
204
+ return (await postJson(`${baseUrl}${INSPECTO_API_PATHS.SESSION_CLAIM_NEXT}`, {
205
+ ...(args.timeoutMs !== undefined ? { timeoutMs: args.timeoutMs } : {}),
206
+ })) as {
207
+ success: boolean
208
+ timedOut: boolean
209
+ matchedExisting: boolean
210
+ session?: Record<string, unknown>
211
+ event?: string
212
+ }
213
+ },
214
+
215
+ async reply(args) {
216
+ return postJson(
217
+ `${baseUrl}${INSPECTO_API_PATHS.SESSIONS}/${args.sessionId}${INSPECTO_API_PATHS.SESSION_REPLY_SUFFIX}`,
218
+ {
219
+ role: 'agent',
220
+ text: args.text.trim(),
221
+ },
222
+ ) as Promise<Record<string, unknown>>
223
+ },
224
+
225
+ async resolve(args) {
226
+ return postJson(
227
+ `${baseUrl}${INSPECTO_API_PATHS.SESSIONS}/${args.sessionId}${INSPECTO_API_PATHS.SESSION_RESOLVE_SUFFIX}`,
228
+ args.message?.trim() ? { message: args.message.trim() } : {},
229
+ ) as Promise<Record<string, unknown>>
230
+ },
231
+
232
+ async dismiss(args) {
233
+ return postJson(
234
+ `${baseUrl}${INSPECTO_API_PATHS.SESSIONS}/${args.sessionId}${INSPECTO_API_PATHS.SESSION_DISMISS_SUFFIX}`,
235
+ args.message?.trim() ? { message: args.message.trim() } : {},
236
+ ) as Promise<Record<string, unknown>>
237
+ },
238
+ }
239
+ }
240
+
241
+ export function resolveInspectoServerBaseUrl(cwd: string): string | null {
242
+ const ports = resolveServerPorts(cwd)
243
+ const port = ports[0]
244
+ return port ? `http://127.0.0.1:${port}` : null
245
+ }
246
+
247
+ export function resolveServerPorts(cwd: string): number[] {
248
+ const prioritized = readProjectScopedPorts(cwd)
249
+ if (prioritized.length > 0) return prioritized
250
+
251
+ const legacyPortFile = path.join(os.tmpdir(), 'inspecto.port')
252
+ try {
253
+ const raw = fs.readFileSync(legacyPortFile, 'utf-8').trim()
254
+ const port = parseInt(raw, 10)
255
+ if (Number.isInteger(port) && port > 0 && port < 65536) {
256
+ return [port]
257
+ }
258
+ } catch {
259
+ // ignore
260
+ }
261
+
262
+ return Array.from({ length: 23 }, (_, index) => 5678 + index)
263
+ }
264
+
265
+ function toolSuccess<T extends Record<string, unknown>>(value: T): McpToolResult<T> {
266
+ return {
267
+ content: [
268
+ {
269
+ type: 'text',
270
+ text: JSON.stringify(value, null, 2),
271
+ },
272
+ ],
273
+ structuredContent: value,
274
+ }
275
+ }
276
+
277
+ function toolError(error: unknown): McpToolResult<{ success: false; error: string }> {
278
+ const message = error instanceof Error ? error.message : String(error)
279
+ return {
280
+ content: [
281
+ {
282
+ type: 'text',
283
+ text: message,
284
+ },
285
+ ],
286
+ structuredContent: {
287
+ success: false,
288
+ error: message,
289
+ },
290
+ isError: true,
291
+ }
292
+ }
293
+
294
+ async function getJson(url: string): Promise<unknown> {
295
+ const response = await fetch(url)
296
+ const payload = (await response.json().catch(() => ({}))) as Record<string, unknown>
297
+ if (!response.ok) {
298
+ throw new Error(String(payload['error'] ?? `Request failed with status ${response.status}.`))
299
+ }
300
+ return payload
301
+ }
302
+
303
+ async function getSession(baseUrl: string, sessionId: string): Promise<Record<string, unknown>> {
304
+ const trimmed = sessionId.trim()
305
+ if (!trimmed) {
306
+ throw new Error('Session id is required.')
307
+ }
308
+
309
+ const payload = (await getJson(
310
+ `${baseUrl}${INSPECTO_API_PATHS.SESSIONS}/${encodeURIComponent(trimmed)}`,
311
+ )) as {
312
+ success?: boolean
313
+ session?: Record<string, unknown>
314
+ error?: string
315
+ }
316
+
317
+ if (!payload.success || !payload.session) {
318
+ throw new Error(payload.error ?? 'Session not found.')
319
+ }
320
+
321
+ return {
322
+ success: true,
323
+ session: payload.session,
324
+ }
325
+ }
326
+
327
+ async function postJson(url: string, body: Record<string, unknown>): Promise<unknown> {
328
+ const response = await fetch(url, {
329
+ method: 'POST',
330
+ headers: { 'Content-Type': 'application/json' },
331
+ body: JSON.stringify(body),
332
+ })
333
+ const payload = (await response.json().catch(() => ({}))) as Record<string, unknown>
334
+ if (!response.ok || payload['success'] === false) {
335
+ throw new Error(String(payload['error'] ?? `Request failed with status ${response.status}.`))
336
+ }
337
+ return payload
338
+ }
339
+
340
+ function readProjectScopedPorts(cwd: string): number[] {
341
+ const portFile = path.join(os.tmpdir(), 'inspecto.port.json')
342
+ try {
343
+ const raw = fs.readFileSync(portFile, 'utf-8').trim()
344
+ const portData = JSON.parse(raw) as Record<string, number>
345
+ const currentRootHashes = resolveCandidateRootHashes(cwd)
346
+
347
+ const prioritized: number[] = []
348
+ const seen = new Set<number>()
349
+
350
+ for (const rootHash of currentRootHashes) {
351
+ const currentPort = portData[rootHash]
352
+ if (currentPort && !seen.has(currentPort)) {
353
+ prioritized.push(currentPort)
354
+ seen.add(currentPort)
355
+ }
356
+ }
357
+
358
+ for (const port of Object.values(portData)) {
359
+ if (!seen.has(port)) {
360
+ prioritized.push(port)
361
+ seen.add(port)
362
+ }
363
+ }
364
+
365
+ return prioritized
366
+ } catch {
367
+ return []
368
+ }
369
+ }
370
+
371
+ function resolveCandidateRootHashes(cwd: string): string[] {
372
+ const normalized = path.resolve(cwd)
373
+ const candidates = new Set<string>()
374
+ let currentDir = normalized
375
+
376
+ while (true) {
377
+ candidates.add(crypto.createHash('md5').update(currentDir).digest('hex'))
378
+ const parentDir = path.dirname(currentDir)
379
+ if (parentDir === currentDir) {
380
+ break
381
+ }
382
+ currentDir = parentDir
383
+ }
384
+
385
+ return [...candidates]
386
+ }
@@ -57,6 +57,7 @@ function buildAssistantHandoff(
57
57
  ...(result.pendingSteps ? { pendingSteps: result.pendingSteps } : {}),
58
58
  ...(result.assistantPrompt ? { assistantPrompt: result.assistantPrompt } : {}),
59
59
  ...(result.patches ? { patches: result.patches } : {}),
60
+ ...(result.dailyUsage ? { dailyUsage: result.dailyUsage } : {}),
60
61
  }
61
62
  }
62
63
 
@@ -110,6 +111,9 @@ function printOnboardResult(result: OnboardCommandResult): void {
110
111
  if (normalized.handoff?.assistantPrompt) {
111
112
  log.hint(normalized.handoff.assistantPrompt)
112
113
  }
114
+ if (normalized.handoff?.dailyUsage?.prompt) {
115
+ log.hint(normalized.handoff.dailyUsage.prompt)
116
+ }
113
117
  if (normalized.confirmation.required && normalized.confirmation.question) {
114
118
  log.warn(normalized.confirmation.question)
115
119
  }
@@ -139,7 +143,7 @@ export async function onboard(options: OnboardCommandOptions = {}): Promise<Onbo
139
143
  session.status === 'needs_confirmation'
140
144
  ) {
141
145
  return writeCommandOutput(
142
- normalizeOnboardResult(buildDeferredOnboardResult(session)),
146
+ normalizeOnboardResult(await buildDeferredOnboardResult(session)),
143
147
  options.json ?? false,
144
148
  printOnboardResult,
145
149
  )