@ishlabs/cli 0.17.7 → 0.18.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.
Files changed (62) hide show
  1. package/README.md +54 -54
  2. package/dist/commands/ask.d.ts +4 -4
  3. package/dist/commands/ask.js +66 -66
  4. package/dist/commands/chat.js +10 -10
  5. package/dist/commands/config.js +1 -1
  6. package/dist/commands/docs.js +1 -1
  7. package/dist/commands/iteration.js +57 -57
  8. package/dist/commands/mcp.d.ts +23 -0
  9. package/dist/commands/mcp.js +676 -0
  10. package/dist/commands/person.d.ts +5 -0
  11. package/dist/commands/{profile.js → person.js} +197 -162
  12. package/dist/commands/source.d.ts +6 -2
  13. package/dist/commands/source.js +35 -30
  14. package/dist/commands/study-analyze.d.ts +1 -1
  15. package/dist/commands/study-analyze.js +3 -3
  16. package/dist/commands/study-participant.d.ts +8 -0
  17. package/dist/commands/{study-tester.js → study-participant.js} +50 -50
  18. package/dist/commands/study-run.d.ts +6 -6
  19. package/dist/commands/study-run.js +295 -271
  20. package/dist/commands/study.js +89 -66
  21. package/dist/commands/workspace.js +13 -13
  22. package/dist/connect.js +5 -5
  23. package/dist/index.js +6 -4
  24. package/dist/lib/accessibility-profile.d.ts +1 -1
  25. package/dist/lib/accessibility-profile.js +1 -1
  26. package/dist/lib/alias-hydrate.js +4 -4
  27. package/dist/lib/alias-store.d.ts +5 -5
  28. package/dist/lib/alias-store.js +8 -8
  29. package/dist/lib/api-client.d.ts +1 -1
  30. package/dist/lib/api-client.js +1 -1
  31. package/dist/lib/billing.d.ts +11 -11
  32. package/dist/lib/billing.js +16 -16
  33. package/dist/lib/chat-endpoint-templates.js +1 -1
  34. package/dist/lib/command-helpers.d.ts +18 -18
  35. package/dist/lib/command-helpers.js +49 -37
  36. package/dist/lib/docs.js +560 -386
  37. package/dist/lib/enums.d.ts +2 -2
  38. package/dist/lib/enums.js +2 -2
  39. package/dist/lib/local-sim/browser.d.ts +1 -1
  40. package/dist/lib/local-sim/browser.js +1 -1
  41. package/dist/lib/local-sim/debug-report.d.ts +2 -2
  42. package/dist/lib/local-sim/debug-report.js +3 -3
  43. package/dist/lib/local-sim/loop.d.ts +5 -5
  44. package/dist/lib/local-sim/loop.js +38 -38
  45. package/dist/lib/local-sim/types.d.ts +12 -12
  46. package/dist/lib/mcp-clients.d.ts +51 -0
  47. package/dist/lib/mcp-clients.js +175 -0
  48. package/dist/lib/modality.d.ts +10 -10
  49. package/dist/lib/modality.js +46 -46
  50. package/dist/lib/output.d.ts +13 -12
  51. package/dist/lib/output.js +244 -184
  52. package/dist/lib/profile-sources.d.ts +64 -16
  53. package/dist/lib/profile-sources.js +91 -30
  54. package/dist/lib/skill-content.js +215 -168
  55. package/dist/lib/study-events.d.ts +3 -3
  56. package/dist/lib/study-events.js +1 -1
  57. package/dist/lib/study-inputs.d.ts +11 -1
  58. package/dist/lib/study-inputs.js +68 -17
  59. package/dist/lib/types.d.ts +105 -34
  60. package/package.json +1 -1
  61. package/dist/commands/profile.d.ts +0 -5
  62. package/dist/commands/study-tester.d.ts +0 -8
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Agents (and humans) reach for hyphen-style values on the command line —
5
5
  * `--screen-format mobile-portrait`, `--kind text-file`, `--chat-mode
6
- * tester-pair` — even when the canonical backend value is underscored (or
6
+ * participant-pair` — even when the canonical backend value is underscored (or
7
7
  * vice versa for the ask-question `type` field, which is hyphenated). Rather
8
8
  * than fail with a 422, each parse site funnels the raw value through
9
9
  * `normalizeEnumValue` and gets back the canonical form (or `null` for a
@@ -32,7 +32,7 @@ export type ScreenFormat = typeof SCREEN_FORMATS[number];
32
32
  export declare const QUESTION_TYPES: readonly ["text", "slider", "likert", "single-choice", "multiple-choice", "number"];
33
33
  export type QuestionType = typeof QUESTION_TYPES[number];
34
34
  /**
35
- * TesterProfile structured enums (profile-enums.v1.json). Values are
35
+ * Person structured enums (profile-enums.v1.json). Values are
36
36
  * snake_case and match the spec byte-for-byte; agents pass them verbatim
37
37
  * via CLI flags.
38
38
  */
package/dist/lib/enums.js CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Agents (and humans) reach for hyphen-style values on the command line —
5
5
  * `--screen-format mobile-portrait`, `--kind text-file`, `--chat-mode
6
- * tester-pair` — even when the canonical backend value is underscored (or
6
+ * participant-pair` — even when the canonical backend value is underscored (or
7
7
  * vice versa for the ask-question `type` field, which is hyphenated). Rather
8
8
  * than fail with a 422, each parse site funnels the raw value through
9
9
  * `normalizeEnumValue` and gets back the canonical form (or `null` for a
@@ -46,7 +46,7 @@ export const QUESTION_TYPES = [
46
46
  "number",
47
47
  ];
48
48
  /**
49
- * TesterProfile structured enums (profile-enums.v1.json). Values are
49
+ * Person structured enums (profile-enums.v1.json). Values are
50
50
  * snake_case and match the spec byte-for-byte; agents pass them verbatim
51
51
  * via CLI flags.
52
52
  */
@@ -23,7 +23,7 @@ export declare function launchSharedBrowser(opts: LocalSimBrowserOptions): Promi
23
23
  */
24
24
  export declare function createTab(browser: Browser, opts: LocalSimBrowserOptions): Promise<BrowserSession>;
25
25
  /**
26
- * Launch a standalone browser session (single tester, owns the browser).
26
+ * Launch a standalone browser session (single participant, owns the browser).
27
27
  */
28
28
  export declare function launchBrowser(opts: LocalSimBrowserOptions): Promise<BrowserSession>;
29
29
  export interface ObservationData {
@@ -76,7 +76,7 @@ export async function createTab(browser, opts) {
76
76
  return { browser, context, page };
77
77
  }
78
78
  /**
79
- * Launch a standalone browser session (single tester, owns the browser).
79
+ * Launch a standalone browser session (single participant, owns the browser).
80
80
  */
81
81
  export async function launchBrowser(opts) {
82
82
  const browser = await launchSharedBrowser(opts);
@@ -7,8 +7,8 @@
7
7
  import type { DebugStep } from "./loop.js";
8
8
  export type { DebugStep };
9
9
  export interface DebugReportMeta {
10
- testerId: string;
11
- testerName: string;
10
+ participantId: string;
11
+ participantName: string;
12
12
  url: string;
13
13
  screenFormat: string;
14
14
  finalStatus: string;
@@ -28,7 +28,7 @@ export function generateDebugReport(steps, meta) {
28
28
  const dir = join(homedir(), ".ish", "debug");
29
29
  mkdirSync(dir, { recursive: true });
30
30
  const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
31
- const shortId = meta.testerId.slice(0, 12);
31
+ const shortId = meta.participantId.slice(0, 12);
32
32
  const fileName = `sim-${shortId}-${ts}.html`;
33
33
  const filePath = join(dir, fileName);
34
34
  const totalSteps = steps.length;
@@ -119,7 +119,7 @@ export function generateDebugReport(steps, meta) {
119
119
  <html lang="en">
120
120
  <head>
121
121
  <meta charset="utf-8"/>
122
- <title>Local Sim Debug — ${escapeHtml(meta.testerName)}</title>
122
+ <title>Local Sim Debug — ${escapeHtml(meta.participantName)}</title>
123
123
  <style>
124
124
  * { box-sizing: border-box; margin: 0; padding: 0; }
125
125
  body { background: #1a1a2e; color: #e0e0e0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace; padding: 20px; }
@@ -169,7 +169,7 @@ export function generateDebugReport(steps, meta) {
169
169
  <div class="header">
170
170
  <h1>Local Sim Debug Report</h1>
171
171
  <div class="header-meta">
172
- <span><strong>Tester:</strong> ${escapeHtml(meta.testerName)} (${escapeHtml(meta.testerId.slice(0, 12))})</span>
172
+ <span><strong>Participant:</strong> ${escapeHtml(meta.participantName)} (${escapeHtml(meta.participantId.slice(0, 12))})</span>
173
173
  <span><strong>URL:</strong> ${escapeHtml(meta.url)}</span>
174
174
  <span><strong>Format:</strong> ${escapeHtml(meta.screenFormat)}</span>
175
175
  <span><strong>Status:</strong> <span class="badge" style="background:${meta.finalStatus === "completed" ? "#4caf50" : meta.finalStatus === "failed" ? "#f44336" : "#ff9800"}">${escapeHtml(meta.finalStatus)}</span></span>
@@ -2,7 +2,7 @@
2
2
  * Local simulation loop orchestrator.
3
3
  *
4
4
  * Runs the observe → reason (remote) → act (local) loop for each
5
- * tester against a local Playwright browser.
5
+ * participant against a local Playwright browser.
6
6
  */
7
7
  import type { ApiClient } from "../api-client.js";
8
8
  export interface DebugStep {
@@ -39,8 +39,8 @@ export interface LocalSimRunOptions {
39
39
  workspaceId: string;
40
40
  studyId: string;
41
41
  iterationId: string;
42
- testerIds: string[];
43
- testerNames: Map<string, string>;
42
+ participantIds: string[];
43
+ participantNames: Map<string, string>;
44
44
  url?: string;
45
45
  screenFormat?: "desktop" | "mobile_portrait";
46
46
  locale?: string;
@@ -54,7 +54,7 @@ export interface LocalSimRunOptions {
54
54
  parallel?: number;
55
55
  }
56
56
  /**
57
- * Run local simulations — parallel when multiple testers, sequential by default.
58
- * Use --parallel <n> to control concurrency (default: number of testers).
57
+ * Run local simulations — parallel when multiple participants, sequential by default.
58
+ * Use --parallel <n> to control concurrency (default: number of participants).
59
59
  */
60
60
  export declare function runLocalSimulations(client: ApiClient, opts: LocalSimRunOptions): Promise<void>;
@@ -2,7 +2,7 @@
2
2
  * Local simulation loop orchestrator.
3
3
  *
4
4
  * Runs the observe → reason (remote) → act (local) loop for each
5
- * tester against a local Playwright browser.
5
+ * participant against a local Playwright browser.
6
6
  */
7
7
  import { launchBrowser, launchSharedBrowser, createTab, captureObservation, takeScreenshot, takeScreenshotJpeg, navigateWithRetry, closeBrowser } from "./browser.js";
8
8
  import { uploadScreenshot } from "./upload.js";
@@ -71,8 +71,8 @@ const SENTIMENT_ICONS = {
71
71
  Frustrated: "!", Confused: "?", Delighted: "*",
72
72
  };
73
73
  /**
74
- * Run local simulations — parallel when multiple testers, sequential by default.
75
- * Use --parallel <n> to control concurrency (default: number of testers).
74
+ * Run local simulations — parallel when multiple participants, sequential by default.
75
+ * Use --parallel <n> to control concurrency (default: number of participants).
76
76
  */
77
77
  export async function runLocalSimulations(client, opts) {
78
78
  const log = (msg) => { if (!opts.quiet || opts.debug)
@@ -89,29 +89,29 @@ export async function runLocalSimulations(client, opts) {
89
89
  log("\nCancelling after current step...");
90
90
  };
91
91
  process.on("SIGINT", onSigint);
92
- const concurrency = opts.parallel ?? opts.testerIds.length;
92
+ const concurrency = opts.parallel ?? opts.participantIds.length;
93
93
  try {
94
- if (concurrency <= 1 || opts.testerIds.length <= 1) {
95
- // Sequential execution — each tester owns its own browser
96
- for (const testerId of opts.testerIds) {
94
+ if (concurrency <= 1 || opts.participantIds.length <= 1) {
95
+ // Sequential execution — each participant owns its own browser
96
+ for (const participantId of opts.participantIds) {
97
97
  if (cancelled)
98
98
  break;
99
- const testerName = opts.testerNames.get(testerId) ?? testerId;
100
- log(`\nStarting local simulation for ${testerName}...`);
99
+ const participantName = opts.participantNames.get(participantId) ?? participantId;
100
+ log(`\nStarting local simulation for ${participantName}...`);
101
101
  try {
102
- const testerLog = (msg) => log(`[${testerName}] ${msg}`);
103
- await runSingleSimulation(client, testerId, testerName, opts, testerLog, () => cancelled);
104
- log(`Completed: ${testerName}`);
102
+ const participantLog = (msg) => log(`[${participantName}] ${msg}`);
103
+ await runSingleSimulation(client, participantId, participantName, opts, participantLog, () => cancelled);
104
+ log(`Completed: ${participantName}`);
105
105
  }
106
106
  catch (err) {
107
107
  const msg = err instanceof Error ? err.message : String(err);
108
- log(`Failed: ${testerName} — ${msg}`);
108
+ log(`Failed: ${participantName} — ${msg}`);
109
109
  }
110
110
  }
111
111
  }
112
112
  else {
113
- // Parallel execution — shared browser, one tab per tester
114
- log(`\nRunning ${opts.testerIds.length} simulations in parallel (concurrency: ${concurrency})...`);
113
+ // Parallel execution — shared browser, one tab per participant
114
+ log(`\nRunning ${opts.participantIds.length} simulations in parallel (concurrency: ${concurrency})...`);
115
115
  const sharedBrowserOpts = {
116
116
  headed: opts.headed,
117
117
  slowMo: opts.slowMo,
@@ -123,23 +123,23 @@ export async function runLocalSimulations(client, opts) {
123
123
  const sharedBrowser = await launchSharedBrowser(sharedBrowserOpts);
124
124
  try {
125
125
  const batches = [];
126
- for (let i = 0; i < opts.testerIds.length; i += concurrency) {
127
- batches.push(opts.testerIds.slice(i, i + concurrency));
126
+ for (let i = 0; i < opts.participantIds.length; i += concurrency) {
127
+ batches.push(opts.participantIds.slice(i, i + concurrency));
128
128
  }
129
129
  for (const batch of batches) {
130
130
  if (cancelled)
131
131
  break;
132
- const promises = batch.map(async (testerId) => {
133
- const testerName = opts.testerNames.get(testerId) ?? testerId;
134
- const testerLog = (msg) => log(`[${testerName}] ${msg}`);
135
- testerLog("Starting...");
132
+ const promises = batch.map(async (participantId) => {
133
+ const participantName = opts.participantNames.get(participantId) ?? participantId;
134
+ const participantLog = (msg) => log(`[${participantName}] ${msg}`);
135
+ participantLog("Starting...");
136
136
  try {
137
- await runSingleSimulation(client, testerId, testerName, opts, testerLog, () => cancelled, sharedBrowser);
138
- testerLog("Completed");
137
+ await runSingleSimulation(client, participantId, participantName, opts, participantLog, () => cancelled, sharedBrowser);
138
+ participantLog("Completed");
139
139
  }
140
140
  catch (err) {
141
141
  const msg = err instanceof Error ? err.message : String(err);
142
- testerLog(`Failed — ${msg}`);
142
+ participantLog(`Failed — ${msg}`);
143
143
  }
144
144
  });
145
145
  await Promise.allSettled(promises);
@@ -154,10 +154,10 @@ export async function runLocalSimulations(client, opts) {
154
154
  process.off("SIGINT", onSigint);
155
155
  }
156
156
  }
157
- async function runSingleSimulation(client, testerId, testerName, opts, log, isCancelled, sharedBrowser) {
157
+ async function runSingleSimulation(client, participantId, participantName, opts, log, isCancelled, sharedBrowser) {
158
158
  // Step 1: Initialize session
159
159
  const initResponse = await client.localSimInit({
160
- tester_id: testerId,
160
+ participant_id: participantId,
161
161
  study_id: opts.studyId,
162
162
  product_id: opts.workspaceId,
163
163
  iteration_id: opts.iterationId,
@@ -172,12 +172,12 @@ async function runSingleSimulation(client, testerId, testerName, opts, log, isCa
172
172
  const locale = opts.locale ?? iterDetails?.locale;
173
173
  // Cache session state for per-step requests
174
174
  const session = {
175
- tester_id: initResponse.tester_id,
175
+ participant_id: initResponse.participant_id,
176
176
  study_id: initResponse.study_id,
177
177
  product_id: initResponse.product_id,
178
178
  assignments: initResponse.assignments,
179
- tester_background: initResponse.tester_background,
180
- tester_language: initResponse.tester_language,
179
+ participant_background: initResponse.participant_background,
180
+ participant_language: initResponse.participant_language,
181
181
  context_values: initResponse.context_values,
182
182
  max_interactions: initResponse.max_interactions,
183
183
  agent_model: initResponse.agent_model,
@@ -251,7 +251,7 @@ async function runSingleSimulation(client, testerId, testerName, opts, log, isCa
251
251
  let stepResponse;
252
252
  try {
253
253
  const stepReqBody = {
254
- tester_id: session.tester_id,
254
+ participant_id: session.participant_id,
255
255
  product_id: session.product_id,
256
256
  assignment_name: assignment.name,
257
257
  assignment_instructions: assignment.instructions,
@@ -263,8 +263,8 @@ async function runSingleSimulation(client, testerId, testerName, opts, log, isCa
263
263
  interaction_count: step,
264
264
  history,
265
265
  forwards,
266
- tester_background: session.tester_background,
267
- tester_language: session.tester_language,
266
+ participant_background: session.participant_background,
267
+ participant_language: session.participant_language,
268
268
  context_values: stepContextValues,
269
269
  agent_model: session.agent_model,
270
270
  dom_model: session.dom_model,
@@ -279,7 +279,7 @@ async function runSingleSimulation(client, testerId, testerName, opts, log, isCa
279
279
  await page.waitForTimeout(2000);
280
280
  try {
281
281
  const stepReqBody = {
282
- tester_id: session.tester_id,
282
+ participant_id: session.participant_id,
283
283
  product_id: session.product_id,
284
284
  assignment_name: assignment.name,
285
285
  assignment_instructions: assignment.instructions,
@@ -291,8 +291,8 @@ async function runSingleSimulation(client, testerId, testerName, opts, log, isCa
291
291
  interaction_count: step,
292
292
  history,
293
293
  forwards,
294
- tester_background: session.tester_background,
295
- tester_language: session.tester_language,
294
+ participant_background: session.participant_background,
295
+ participant_language: session.participant_language,
296
296
  context_values: stepContextValues,
297
297
  agent_model: session.agent_model,
298
298
  dom_model: session.dom_model,
@@ -516,8 +516,8 @@ async function runSingleSimulation(client, testerId, testerName, opts, log, isCa
516
516
  try {
517
517
  const { generateDebugReport } = await import("./debug-report.js");
518
518
  generateDebugReport(debugSteps, {
519
- testerId: session.tester_id,
520
- testerName,
519
+ participantId: session.participant_id,
520
+ participantName,
521
521
  url: navigationUrl,
522
522
  screenFormat,
523
523
  finalStatus,
@@ -531,7 +531,7 @@ async function runSingleSimulation(client, testerId, testerName, opts, log, isCa
531
531
  }
532
532
  try {
533
533
  await client.localSimRecord({
534
- tester_id: session.tester_id,
534
+ participant_id: session.participant_id,
535
535
  product_id: session.product_id,
536
536
  interactions,
537
537
  final_status: finalStatus,
@@ -6,7 +6,7 @@
6
6
  * receives actions with node_ids, resolves elements locally via CDP.
7
7
  */
8
8
  export interface LocalSimInitRequest {
9
- tester_id: string;
9
+ participant_id: string;
10
10
  study_id: string;
11
11
  product_id: string;
12
12
  iteration_id: string;
@@ -18,12 +18,12 @@ export interface IterationDetails {
18
18
  locale?: string;
19
19
  }
20
20
  export interface LocalSimInitResponse {
21
- tester_id: string;
21
+ participant_id: string;
22
22
  study_id: string;
23
23
  product_id: string;
24
24
  assignments: LocalSimAssignment[];
25
- tester_background: Record<string, unknown> | null;
26
- tester_language: string | null;
25
+ participant_background: Record<string, unknown> | null;
26
+ participant_language: string | null;
27
27
  config: Record<string, unknown>;
28
28
  context_values: ContextValue[];
29
29
  max_interactions: number;
@@ -64,7 +64,7 @@ export interface LocalTabInfo {
64
64
  opener_id: string | null;
65
65
  }
66
66
  export interface LocalSimStepRequest {
67
- tester_id: string;
67
+ participant_id: string;
68
68
  product_id: string;
69
69
  assignment_name: string;
70
70
  assignment_instructions: string;
@@ -76,8 +76,8 @@ export interface LocalSimStepRequest {
76
76
  interaction_count: number;
77
77
  history: HistoryEntry[];
78
78
  forwards: ForwardEntry[];
79
- tester_background: Record<string, unknown> | null;
80
- tester_language: string | null;
79
+ participant_background: Record<string, unknown> | null;
80
+ participant_language: string | null;
81
81
  context_values: ContextValue[];
82
82
  agent_model: string | null;
83
83
  dom_model: string | null;
@@ -190,14 +190,14 @@ export interface AssignmentStatusUpdate {
190
190
  step_count: number;
191
191
  }
192
192
  export interface LocalSimRecordRequest {
193
- tester_id: string;
193
+ participant_id: string;
194
194
  product_id: string;
195
195
  interactions: RecordInteraction[];
196
196
  final_status: string;
197
197
  assignment_statuses: AssignmentStatusUpdate[];
198
198
  }
199
199
  export interface LocalSimRecordResponse {
200
- tester_id: string;
200
+ participant_id: string;
201
201
  interactions_created: number;
202
202
  credits_consumed: number;
203
203
  }
@@ -233,12 +233,12 @@ export interface TreeData {
233
233
  * Avoids DB lookups on the backend hot path.
234
234
  */
235
235
  export interface SessionState {
236
- tester_id: string;
236
+ participant_id: string;
237
237
  study_id: string;
238
238
  product_id: string;
239
239
  assignments: LocalSimAssignment[];
240
- tester_background: Record<string, unknown> | null;
241
- tester_language: string | null;
240
+ participant_background: Record<string, unknown> | null;
241
+ participant_language: string | null;
242
242
  context_values: ContextValue[];
243
243
  max_interactions: number;
244
244
  agent_model: string;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * MCP client targets — the five clients `ish mcp add` knows how to wire.
3
+ *
4
+ * Mirrors the SkillTargetSpec shape in `skill-content.ts` (a per-client
5
+ * table with config-path resolver + detection predicate). Each entry
6
+ * captures the surface required to round-trip the client's config file:
7
+ *
8
+ * - `configPath()` — absolute path to the client's MCP config (per-OS).
9
+ * - `detect()` — does the per-client config directory exist?
10
+ * Accepts false-negatives on never-launched apps;
11
+ * we'd rather skip than write into a bogus dir.
12
+ * - `serverKey` — which top-level object holds servers in this
13
+ * client's config (`mcpServers` for most;
14
+ * `servers` for VS Code).
15
+ * - `renderServer()` — the per-client server-block shape (the actual
16
+ * JSON value placed under `<serverKey>.<name>`).
17
+ *
18
+ * Server URL is sourced from `ISH_MCP_URL` (env override) or the public
19
+ * `https://mcp.ishlabs.io/mcp` default. Honors `--dev` indirectly: callers
20
+ * can flip the env var before invoking; the hosted MCP server handles
21
+ * OAuth on first connect so we never embed credentials in client files.
22
+ */
23
+ /** Public default. Override with `ISH_MCP_URL` (used by `--dev` workflows). */
24
+ export declare const ISH_MCP_URL: string;
25
+ /** Server name placed under `<serverKey>` in each client config. */
26
+ export declare const ISH_SERVER_NAME = "ish";
27
+ export type McpClientKey = "cursor" | "vscode" | "claude-code" | "claude-desktop" | "windsurf";
28
+ export type McpServerKey = "mcpServers" | "servers";
29
+ export interface McpClientSpec {
30
+ key: McpClientKey;
31
+ displayName: string;
32
+ /** Top-level object that holds MCP servers in this client's config. */
33
+ serverKey: McpServerKey;
34
+ /** Absolute path to this client's MCP config file (per-OS). */
35
+ configPath: () => string;
36
+ /** Cheap detection: does the per-client config dir exist on disk? */
37
+ detect: () => boolean;
38
+ /** Per-client server-block shape (the value at `<serverKey>.<name>`). */
39
+ renderServer: (url: string) => Record<string, unknown>;
40
+ /** True when this client can't be wired on the current OS. */
41
+ unsupportedOnThisOs?: boolean;
42
+ }
43
+ /**
44
+ * Per-client targets. Order matters only for human output (listed in this
45
+ * order in `mcp list` and `mcp add` dry-run plans).
46
+ */
47
+ export declare const MCP_CLIENT_TARGETS: McpClientSpec[];
48
+ /** Lookup by key (returns undefined for unknown keys). */
49
+ export declare function getClientSpec(key: string): McpClientSpec | undefined;
50
+ /** Convenience: every valid client key, for usage-error messages. */
51
+ export declare function allClientKeys(): McpClientKey[];
@@ -0,0 +1,175 @@
1
+ /**
2
+ * MCP client targets — the five clients `ish mcp add` knows how to wire.
3
+ *
4
+ * Mirrors the SkillTargetSpec shape in `skill-content.ts` (a per-client
5
+ * table with config-path resolver + detection predicate). Each entry
6
+ * captures the surface required to round-trip the client's config file:
7
+ *
8
+ * - `configPath()` — absolute path to the client's MCP config (per-OS).
9
+ * - `detect()` — does the per-client config directory exist?
10
+ * Accepts false-negatives on never-launched apps;
11
+ * we'd rather skip than write into a bogus dir.
12
+ * - `serverKey` — which top-level object holds servers in this
13
+ * client's config (`mcpServers` for most;
14
+ * `servers` for VS Code).
15
+ * - `renderServer()` — the per-client server-block shape (the actual
16
+ * JSON value placed under `<serverKey>.<name>`).
17
+ *
18
+ * Server URL is sourced from `ISH_MCP_URL` (env override) or the public
19
+ * `https://mcp.ishlabs.io/mcp` default. Honors `--dev` indirectly: callers
20
+ * can flip the env var before invoking; the hosted MCP server handles
21
+ * OAuth on first connect so we never embed credentials in client files.
22
+ */
23
+ import * as os from "node:os";
24
+ import * as path from "node:path";
25
+ import * as fs from "node:fs";
26
+ /** Public default. Override with `ISH_MCP_URL` (used by `--dev` workflows). */
27
+ export const ISH_MCP_URL = process.env.ISH_MCP_URL ?? "https://mcp.ishlabs.io/mcp";
28
+ /** Server name placed under `<serverKey>` in each client config. */
29
+ export const ISH_SERVER_NAME = "ish";
30
+ function homedir() {
31
+ return os.homedir();
32
+ }
33
+ function appData() {
34
+ return process.env.APPDATA ?? path.join(homedir(), "AppData", "Roaming");
35
+ }
36
+ function dirExists(p) {
37
+ try {
38
+ return fs.statSync(p).isDirectory();
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ }
44
+ // --- Cursor ---------------------------------------------------------------
45
+ //
46
+ // Cross-platform: `~/.cursor/mcp.json`.
47
+ function cursorConfigPath() {
48
+ return path.join(homedir(), ".cursor", "mcp.json");
49
+ }
50
+ // --- VS Code --------------------------------------------------------------
51
+ //
52
+ // macOS: ~/Library/Application Support/Code/User/mcp.json
53
+ // Linux: ~/.config/Code/User/mcp.json
54
+ // Windows: %APPDATA%\Code\User\mcp.json
55
+ function vscodeConfigDir() {
56
+ const plat = os.platform();
57
+ if (plat === "darwin") {
58
+ return path.join(homedir(), "Library", "Application Support", "Code", "User");
59
+ }
60
+ if (plat === "win32") {
61
+ return path.join(appData(), "Code", "User");
62
+ }
63
+ return path.join(homedir(), ".config", "Code", "User");
64
+ }
65
+ function vscodeConfigPath() {
66
+ return path.join(vscodeConfigDir(), "mcp.json");
67
+ }
68
+ // --- Claude Code (user-scope) --------------------------------------------
69
+ //
70
+ // `~/.claude.json` is Claude Code's user-scope MCP config. (`--scope project`
71
+ // writes `.mcp.json` in the project root — we don't drive that here.)
72
+ function claudeCodeConfigPath() {
73
+ return path.join(homedir(), ".claude.json");
74
+ }
75
+ // --- Claude Desktop ------------------------------------------------------
76
+ //
77
+ // macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
78
+ // Windows: %APPDATA%\Claude\claude_desktop_config.json
79
+ // Linux: not supported by Anthropic at the time of writing.
80
+ function claudeDesktopConfigDir() {
81
+ const plat = os.platform();
82
+ if (plat === "darwin") {
83
+ return path.join(homedir(), "Library", "Application Support", "Claude");
84
+ }
85
+ if (plat === "win32") {
86
+ return path.join(appData(), "Claude");
87
+ }
88
+ // Linux: no official desktop release. Report a sentinel path; the spec
89
+ // also flips `unsupportedOnThisOs` so callers can skip without writing
90
+ // a config under a dir Claude Desktop won't ever read.
91
+ return path.join(homedir(), ".config", "Claude");
92
+ }
93
+ function claudeDesktopConfigPath() {
94
+ return path.join(claudeDesktopConfigDir(), "claude_desktop_config.json");
95
+ }
96
+ // --- Windsurf -------------------------------------------------------------
97
+ //
98
+ // `~/.codeium/windsurf/mcp_config.json` — same on every platform.
99
+ function windsurfConfigPath() {
100
+ return path.join(homedir(), ".codeium", "windsurf", "mcp_config.json");
101
+ }
102
+ /**
103
+ * Per-client targets. Order matters only for human output (listed in this
104
+ * order in `mcp list` and `mcp add` dry-run plans).
105
+ */
106
+ export const MCP_CLIENT_TARGETS = [
107
+ {
108
+ key: "cursor",
109
+ displayName: "Cursor",
110
+ serverKey: "mcpServers",
111
+ configPath: cursorConfigPath,
112
+ detect: () => dirExists(path.join(homedir(), ".cursor")),
113
+ renderServer: (url) => ({ url }),
114
+ },
115
+ {
116
+ key: "vscode",
117
+ displayName: "VS Code",
118
+ serverKey: "servers",
119
+ configPath: vscodeConfigPath,
120
+ detect: () => dirExists(vscodeConfigDir()),
121
+ renderServer: (url) => ({ type: "http", url }),
122
+ },
123
+ {
124
+ key: "claude-code",
125
+ displayName: "Claude Code",
126
+ serverKey: "mcpServers",
127
+ configPath: claudeCodeConfigPath,
128
+ // Claude Code's user-scope config is a single file at `~/.claude.json`.
129
+ // Detect by file existence (rather than dir existence) so users who
130
+ // have only ever installed Claude Code via npm — and never created a
131
+ // `~/.claude/` dir — still get the entry surfaced.
132
+ detect: () => {
133
+ try {
134
+ return fs.existsSync(claudeCodeConfigPath()) || dirExists(path.join(homedir(), ".claude"));
135
+ }
136
+ catch {
137
+ return false;
138
+ }
139
+ },
140
+ renderServer: (url) => ({ type: "http", url }),
141
+ },
142
+ {
143
+ key: "claude-desktop",
144
+ displayName: "Claude Desktop",
145
+ serverKey: "mcpServers",
146
+ configPath: claudeDesktopConfigPath,
147
+ detect: () => {
148
+ // No Linux release yet — surface as undetected on linux to keep the
149
+ // `--all` sweep honest. Users can still target it explicitly via
150
+ // `--client claude-desktop` if they have a config dir there.
151
+ if (os.platform() === "linux")
152
+ return false;
153
+ return dirExists(claudeDesktopConfigDir());
154
+ },
155
+ renderServer: (url) => ({ type: "http", url }),
156
+ unsupportedOnThisOs: os.platform() === "linux",
157
+ },
158
+ {
159
+ key: "windsurf",
160
+ displayName: "Windsurf",
161
+ serverKey: "mcpServers",
162
+ configPath: windsurfConfigPath,
163
+ detect: () => dirExists(path.join(homedir(), ".codeium", "windsurf"))
164
+ || dirExists(path.join(homedir(), ".codeium")),
165
+ renderServer: (url) => ({ serverUrl: url }),
166
+ },
167
+ ];
168
+ /** Lookup by key (returns undefined for unknown keys). */
169
+ export function getClientSpec(key) {
170
+ return MCP_CLIENT_TARGETS.find((c) => c.key === key);
171
+ }
172
+ /** Convenience: every valid client key, for usage-error messages. */
173
+ export function allClientKeys() {
174
+ return MCP_CLIENT_TARGETS.map((c) => c.key);
175
+ }
@@ -29,11 +29,11 @@ export declare function describeRequiredContentFlag(modality: string, chatMode?:
29
29
  * the read helpers below fall back to that legacy shape and treat it as
30
30
  * `external_chatbot`.
31
31
  */
32
- declare const CHAT_MODES: readonly ["external_chatbot", "tester_pair"];
32
+ declare const CHAT_MODES: readonly ["external_chatbot", "participant_pair"];
33
33
  export type ChatMode = typeof CHAT_MODES[number];
34
34
  /**
35
- * Normalise user-supplied `--chat-mode` values. Accepts both `tester_pair`
36
- * (canonical) and `tester-pair` (hyphenated — matches CLI flag convention
35
+ * Normalise user-supplied `--chat-mode` values. Accepts both `participant_pair`
36
+ * (canonical) and `participant-pair` (hyphenated — matches CLI flag convention
37
37
  * elsewhere in `ish`); same for `external-chatbot`. Returns `null` for
38
38
  * anything unrecognised so callers can throw a clean ValidationError.
39
39
  */
@@ -50,14 +50,14 @@ export declare function readExternalChatbotEndpoint(details: unknown): {
50
50
  chatbot_endpoint_id?: string;
51
51
  };
52
52
  /**
53
- * Extract the tester-pair payload from chat details. Returns `undefined` if
54
- * `mode_details.mode !== "tester_pair"`. Does not validate equal lengths or
53
+ * Extract the participant-pair payload from chat details. Returns `undefined` if
54
+ * `mode_details.mode !== "participant_pair"`. Does not validate equal lengths or
55
55
  * non-empty scenarios — that's the caller's job (see `iterationHasContent`
56
56
  * and `validateIterationDetails`).
57
57
  */
58
- export declare function readTesterPairConfig(details: unknown): {
59
- audience_a: string[];
60
- audience_b: string[];
58
+ export declare function readParticipantPairConfig(details: unknown): {
59
+ group_a: string[];
60
+ group_b: string[];
61
61
  scenario_a: string;
62
62
  scenario_b: string;
63
63
  initiator_side: "a" | "b";
@@ -65,8 +65,8 @@ export declare function readTesterPairConfig(details: unknown): {
65
65
  role_criteria_b?: Record<string, unknown>;
66
66
  } | undefined;
67
67
  /**
68
- * RoleCriteria — persona-first filter that resolves a tester-profile pool
69
- * for one side of a tester_pair iteration. Mirrors the backend Pydantic
68
+ * RoleCriteria — persona-first filter that resolves a person pool
69
+ * for one side of a participant_pair iteration. Mirrors the backend Pydantic
70
70
  * shape in `app/api/models/iterations.py:RoleCriteria` (swift-charm plan).
71
71
  * All fields optional; an empty object is treated as "no filter".
72
72
  */