@vibe-interviewing/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 vibe-interviewing contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,472 @@
1
+ import { z } from 'zod';
2
+
3
+ declare const AIRulesSchema: z.ZodObject<{
4
+ /** Role description for the AI assistant */
5
+ role: z.ZodString;
6
+ /** Behavioral rules (e.g., "don't reveal the answer") */
7
+ rules: z.ZodArray<z.ZodString, "many">;
8
+ /** Knowledge about the bug/solution (hidden from candidate) */
9
+ knowledge: z.ZodString;
10
+ }, "strip", z.ZodTypeAny, {
11
+ role: string;
12
+ rules: string[];
13
+ knowledge: string;
14
+ }, {
15
+ role: string;
16
+ rules: string[];
17
+ knowledge: string;
18
+ }>;
19
+ declare const EvaluationSchema: z.ZodObject<{
20
+ /** Evaluation criteria for the interviewer */
21
+ criteria: z.ZodArray<z.ZodString, "many">;
22
+ /** Description of the expected fix */
23
+ expected_fix: z.ZodOptional<z.ZodString>;
24
+ }, "strip", z.ZodTypeAny, {
25
+ criteria: string[];
26
+ expected_fix?: string | undefined;
27
+ }, {
28
+ criteria: string[];
29
+ expected_fix?: string | undefined;
30
+ }>;
31
+ /** Full scenario configuration schema */
32
+ declare const ScenarioConfigSchema: z.ZodObject<{
33
+ /** Scenario display name */
34
+ name: z.ZodString;
35
+ /** One-line description */
36
+ description: z.ZodString;
37
+ /** Difficulty level */
38
+ difficulty: z.ZodEnum<["easy", "medium", "hard"]>;
39
+ /** Estimated time (e.g., "30-45m") */
40
+ estimated_time: z.ZodString;
41
+ /** Searchable tags */
42
+ tags: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
43
+ /** GitHub repo URL or owner/repo shorthand */
44
+ repo: z.ZodString;
45
+ /** Commit SHA to pin the clone to (ensures reproducibility) */
46
+ commit: z.ZodString;
47
+ /** Shell commands to run after cloning (e.g., ["npm install"]) */
48
+ setup: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
49
+ /** Find-and-replace patches to inject the bug after cloning */
50
+ patch: z.ZodDefault<z.ZodArray<z.ZodObject<{
51
+ /** Path to the file relative to repo root */
52
+ file: z.ZodString;
53
+ /** The original text to find */
54
+ find: z.ZodString;
55
+ /** The replacement text (with the bug) */
56
+ replace: z.ZodString;
57
+ }, "strip", z.ZodTypeAny, {
58
+ find: string;
59
+ file: string;
60
+ replace: string;
61
+ }, {
62
+ find: string;
63
+ file: string;
64
+ replace: string;
65
+ }>, "many">>;
66
+ /** Briefing shown to the candidate (written like a team lead message) */
67
+ briefing: z.ZodString;
68
+ /** AI behavioral rules (injected via system prompt, hidden from candidate) */
69
+ ai_rules: z.ZodObject<{
70
+ /** Role description for the AI assistant */
71
+ role: z.ZodString;
72
+ /** Behavioral rules (e.g., "don't reveal the answer") */
73
+ rules: z.ZodArray<z.ZodString, "many">;
74
+ /** Knowledge about the bug/solution (hidden from candidate) */
75
+ knowledge: z.ZodString;
76
+ }, "strip", z.ZodTypeAny, {
77
+ role: string;
78
+ rules: string[];
79
+ knowledge: string;
80
+ }, {
81
+ role: string;
82
+ rules: string[];
83
+ knowledge: string;
84
+ }>;
85
+ /** Interviewer reference — what the fix looks like */
86
+ solution: z.ZodString;
87
+ /** Evaluation rubric */
88
+ evaluation: z.ZodOptional<z.ZodObject<{
89
+ /** Evaluation criteria for the interviewer */
90
+ criteria: z.ZodArray<z.ZodString, "many">;
91
+ /** Description of the expected fix */
92
+ expected_fix: z.ZodOptional<z.ZodString>;
93
+ }, "strip", z.ZodTypeAny, {
94
+ criteria: string[];
95
+ expected_fix?: string | undefined;
96
+ }, {
97
+ criteria: string[];
98
+ expected_fix?: string | undefined;
99
+ }>>;
100
+ /** License of the original project */
101
+ license: z.ZodOptional<z.ZodString>;
102
+ }, "strip", z.ZodTypeAny, {
103
+ name: string;
104
+ description: string;
105
+ difficulty: "easy" | "medium" | "hard";
106
+ estimated_time: string;
107
+ tags: string[];
108
+ repo: string;
109
+ commit: string;
110
+ setup: string[];
111
+ patch: {
112
+ find: string;
113
+ file: string;
114
+ replace: string;
115
+ }[];
116
+ briefing: string;
117
+ ai_rules: {
118
+ role: string;
119
+ rules: string[];
120
+ knowledge: string;
121
+ };
122
+ solution: string;
123
+ evaluation?: {
124
+ criteria: string[];
125
+ expected_fix?: string | undefined;
126
+ } | undefined;
127
+ license?: string | undefined;
128
+ }, {
129
+ name: string;
130
+ description: string;
131
+ difficulty: "easy" | "medium" | "hard";
132
+ estimated_time: string;
133
+ repo: string;
134
+ commit: string;
135
+ briefing: string;
136
+ ai_rules: {
137
+ role: string;
138
+ rules: string[];
139
+ knowledge: string;
140
+ };
141
+ solution: string;
142
+ tags?: string[] | undefined;
143
+ setup?: string[] | undefined;
144
+ patch?: {
145
+ find: string;
146
+ file: string;
147
+ replace: string;
148
+ }[] | undefined;
149
+ evaluation?: {
150
+ criteria: string[];
151
+ expected_fix?: string | undefined;
152
+ } | undefined;
153
+ license?: string | undefined;
154
+ }>;
155
+ type ScenarioConfig = z.infer<typeof ScenarioConfigSchema>;
156
+ type AIRules = z.infer<typeof AIRulesSchema>;
157
+ type Evaluation = z.infer<typeof EvaluationSchema>;
158
+ /** Metadata about a discovered scenario */
159
+ interface ScenarioInfo {
160
+ /** Scenario name */
161
+ name: string;
162
+ /** Parsed config */
163
+ config: ScenarioConfig;
164
+ /** Whether this is a built-in scenario */
165
+ builtIn: boolean;
166
+ }
167
+
168
+ /**
169
+ * Load and parse a scenario config from a YAML file.
170
+ *
171
+ * @param configPath - Absolute path to the scenario.yaml file
172
+ * @returns The parsed and validated scenario config
173
+ */
174
+ declare function loadScenarioConfig(configPath: string): Promise<ScenarioConfig>;
175
+ /**
176
+ * Generate a system prompt string from a scenario's ai_rules.
177
+ *
178
+ * This prompt is injected into the AI tool via --append-system-prompt
179
+ * and is hidden from the candidate.
180
+ *
181
+ * @param config - The scenario config containing ai_rules
182
+ * @returns The formatted system prompt
183
+ */
184
+ declare function generateSystemPrompt(config: ScenarioConfig): string;
185
+
186
+ /** Result of validating a scenario configuration */
187
+ interface ValidationResult {
188
+ /** Whether the scenario is valid (no errors) */
189
+ valid: boolean;
190
+ /** Non-fatal issues that should be addressed */
191
+ warnings: string[];
192
+ /** Fatal issues that prevent the scenario from running */
193
+ errors: string[];
194
+ }
195
+ /**
196
+ * Validate a scenario config for completeness and correctness.
197
+ *
198
+ * @param config - The scenario config to validate
199
+ * @returns Validation result with errors and warnings
200
+ */
201
+ declare function validateScenario(config: ScenarioConfig): Promise<ValidationResult>;
202
+ /**
203
+ * Validate a scenario config and throw if invalid.
204
+ *
205
+ * @param config - The scenario config to validate
206
+ * @returns Validation result (only returned if valid)
207
+ * @throws ScenarioValidationError if the config has errors
208
+ */
209
+ declare function validateScenarioOrThrow(config: ScenarioConfig): Promise<ValidationResult>;
210
+
211
+ /**
212
+ * Discover built-in scenarios from the registry.yaml file in the scenarios package.
213
+ *
214
+ * Each entry in the registry points to a scenario directory containing a full
215
+ * scenario.yaml. The registry provides quick lookup metadata, while the full
216
+ * config is loaded from the scenario's own config file.
217
+ *
218
+ * @returns Array of discovered scenario info objects
219
+ */
220
+ declare function discoverBuiltInScenarios(): Promise<ScenarioInfo[]>;
221
+ /**
222
+ * Discover all available scenarios.
223
+ *
224
+ * Currently returns only built-in scenarios from the registry.
225
+ * Local scenario support can be added later.
226
+ *
227
+ * @returns Array of all discovered scenario info objects
228
+ */
229
+ declare function discoverAllScenarios(): Promise<ScenarioInfo[]>;
230
+
231
+ /**
232
+ * Import a repository by cloning it to a local directory.
233
+ *
234
+ * For commit SHAs, uses a shallow fetch to avoid downloading full history.
235
+ *
236
+ * @param repoUrl - GitHub URL, SSH URL, or owner/repo shorthand
237
+ * @param targetPath - Directory to clone into (defaults to a temp directory)
238
+ * @param ref - Optional branch, tag, or commit to checkout
239
+ * @returns The path to the cloned directory
240
+ */
241
+ declare function importRepo(repoUrl: string, targetPath?: string, ref?: string): Promise<string>;
242
+
243
+ /** Event types that can be captured during a session */
244
+ type SessionEventType = 'stdout' | 'stderr' | 'command' | 'note';
245
+ /** A single timestamped event captured during a session */
246
+ interface SessionEvent {
247
+ /** Milliseconds since recording started */
248
+ timestamp: number;
249
+ /** The kind of event */
250
+ type: SessionEventType;
251
+ /** The captured data */
252
+ data: string;
253
+ }
254
+ /** Serialized recording format */
255
+ interface RecordingData {
256
+ /** Session ID this recording belongs to */
257
+ sessionId: string;
258
+ /** ISO string of when recording started */
259
+ startedAt: string;
260
+ /** All captured events */
261
+ events: SessionEvent[];
262
+ }
263
+ /**
264
+ * Records timestamped events during an interview session.
265
+ *
266
+ * Captures stdout, stderr, commands, and notes with millisecond timestamps
267
+ * relative to when the recorder was created.
268
+ */
269
+ declare class SessionRecorder {
270
+ private readonly events;
271
+ private readonly startTime;
272
+ private readonly startedAt;
273
+ constructor();
274
+ /** Record a timestamped event */
275
+ record(type: SessionEventType, data: string): void;
276
+ /** Get all recorded events */
277
+ getEvents(): ReadonlyArray<SessionEvent>;
278
+ /** Serialize the recording to a JSON-compatible object */
279
+ toJSON(sessionId: string): RecordingData;
280
+ /** Create a SessionRecorder pre-populated with events from serialized data */
281
+ static fromJSON(data: RecordingData): SessionRecorder;
282
+ /** Save the recording to disk */
283
+ save(sessionId: string): Promise<void>;
284
+ /** Load a recording from disk */
285
+ static load(sessionId: string): Promise<SessionRecorder>;
286
+ /** List all available recording session IDs */
287
+ static list(): Promise<string[]>;
288
+ }
289
+
290
+ /** Configuration for launching an AI coding tool */
291
+ interface LaunchConfig {
292
+ /** Scenario name for display */
293
+ scenarioName: string;
294
+ /** Path to system prompt file (hidden from candidate) */
295
+ systemPromptPath: string;
296
+ /** Model to use */
297
+ model?: string;
298
+ /** Permission mode for the AI tool */
299
+ permissionMode?: 'default' | 'plan' | 'acceptEdits' | 'bypassPermissions';
300
+ /** Tools to disallow (e.g., WebSearch for fairness) */
301
+ disallowedTools?: string[];
302
+ /** Whether to record stdout/stderr during the session */
303
+ recording?: boolean;
304
+ }
305
+ /** A running AI tool process */
306
+ interface LaunchedProcess {
307
+ /** Wait for the process to exit */
308
+ wait(): Promise<{
309
+ exitCode: number;
310
+ }>;
311
+ /** Kill the process */
312
+ kill(): Promise<void>;
313
+ /** Session recorder, present when recording is enabled */
314
+ recorder?: SessionRecorder;
315
+ }
316
+ /** Interface for AI coding tool launchers */
317
+ interface AIToolLauncher {
318
+ /** Internal name identifier */
319
+ readonly name: string;
320
+ /** Human-readable display name */
321
+ readonly displayName: string;
322
+ /** Check if this tool is installed and accessible */
323
+ isInstalled(): Promise<boolean>;
324
+ /** Get the installed version string */
325
+ getVersion(): Promise<string | null>;
326
+ /** Launch the tool pointed at a working directory */
327
+ launch(workdir: string, config: LaunchConfig): Promise<LaunchedProcess>;
328
+ }
329
+
330
+ /** Launcher for Anthropic's Claude Code CLI */
331
+ declare class ClaudeCodeLauncher implements AIToolLauncher {
332
+ readonly name = "claude-code";
333
+ readonly displayName = "Claude Code";
334
+ /** Check if the claude CLI is installed */
335
+ isInstalled(): Promise<boolean>;
336
+ /** Get the installed Claude Code version */
337
+ getVersion(): Promise<string | null>;
338
+ /** Launch Claude Code in the given working directory with the provided config */
339
+ launch(workdir: string, config: LaunchConfig): Promise<LaunchedProcess>;
340
+ }
341
+
342
+ /** Information about a detected AI coding tool */
343
+ interface DetectedTool {
344
+ /** The launcher instance */
345
+ launcher: AIToolLauncher;
346
+ /** Installed version string, or null if unknown */
347
+ version: string | null;
348
+ }
349
+ /** Detect which AI coding tools are installed on the system */
350
+ declare function detectInstalledTools(): Promise<DetectedTool[]>;
351
+ /** Get a launcher by its internal name */
352
+ declare function getLauncher(name: string): AIToolLauncher | undefined;
353
+ /** Get all registered launchers */
354
+ declare function getAllLaunchers(): AIToolLauncher[];
355
+
356
+ /** Status of an interview session */
357
+ type SessionStatus = 'cloning' | 'setting-up' | 'running' | 'complete';
358
+ /** A live interview session */
359
+ interface Session {
360
+ /** Unique session identifier */
361
+ id: string;
362
+ /** Name of the scenario being run */
363
+ scenarioName: string;
364
+ /** Local working directory for the candidate */
365
+ workdir: string;
366
+ /** Path to the system prompt file (outside workspace) */
367
+ systemPromptPath: string;
368
+ /** Current session status */
369
+ status: SessionStatus;
370
+ /** ISO timestamp of session creation */
371
+ createdAt: string;
372
+ /** ISO timestamp of when the AI tool was launched */
373
+ startedAt?: string;
374
+ /** ISO timestamp of session completion */
375
+ completedAt?: string;
376
+ /** Name of the AI tool used */
377
+ aiTool?: string;
378
+ }
379
+ /** Serializable session data for persistence */
380
+ interface StoredSession {
381
+ /** Unique session identifier */
382
+ id: string;
383
+ /** Name of the scenario being run */
384
+ scenarioName: string;
385
+ /** Current session status */
386
+ status: SessionStatus;
387
+ /** Local working directory for the candidate */
388
+ workdir: string;
389
+ /** Path to the system prompt file */
390
+ systemPromptPath: string;
391
+ /** Name of the AI tool used */
392
+ aiTool?: string;
393
+ /** ISO timestamp of session creation */
394
+ createdAt: string;
395
+ /** ISO timestamp of when the AI tool was launched */
396
+ startedAt?: string;
397
+ /** ISO timestamp of session completion */
398
+ completedAt?: string;
399
+ }
400
+ /** Convert a Session to a StoredSession for persistence */
401
+ declare function toStoredSession(session: Session): StoredSession;
402
+
403
+ /** Callback for reporting session progress */
404
+ type ProgressCallback = (stage: string) => void;
405
+ /** Manages the lifecycle of an interview session */
406
+ declare class SessionManager {
407
+ private launcher;
408
+ constructor(launcher: AIToolLauncher);
409
+ /**
410
+ * Create a new interview session.
411
+ *
412
+ * Flow:
413
+ * 1. Clone the repo at a pinned commit
414
+ * 2. Apply bug patches (find/replace in source files)
415
+ * 3. Wipe git history so the candidate can't diff to find the bug
416
+ * 4. Remove scenario.yaml from workspace (interviewer-only)
417
+ * 5. Write BRIEFING.md and system prompt
418
+ * 6. Run setup commands (npm install, etc.)
419
+ */
420
+ createSession(config: ScenarioConfig, workdir?: string, onProgress?: ProgressCallback): Promise<{
421
+ session: Session;
422
+ config: ScenarioConfig;
423
+ }>;
424
+ /** Launch the AI coding tool for an active session */
425
+ launchAITool(session: Session, _config: ScenarioConfig, launchConfig?: Partial<LaunchConfig>): Promise<{
426
+ exitCode: number;
427
+ }>;
428
+ /** Destroy a session by removing its stored data */
429
+ destroySession(session: Session): Promise<void>;
430
+ /** Get elapsed time since the AI tool was launched, formatted as a human-readable string */
431
+ getElapsedTime(session: Session): string | null;
432
+ }
433
+
434
+ /** Save a session to disk */
435
+ declare function saveSession(session: StoredSession): Promise<void>;
436
+ /** Load a session from disk */
437
+ declare function loadSession(id: string): Promise<StoredSession | null>;
438
+ /** Delete a session from disk */
439
+ declare function deleteSession(id: string): Promise<void>;
440
+ /** List all stored sessions */
441
+ declare function listSessions(): Promise<StoredSession[]>;
442
+ /** List only active (non-complete) sessions */
443
+ declare function listActiveSessions(): Promise<StoredSession[]>;
444
+
445
+ /** Base error class for all vibe-interviewing errors */
446
+ declare class VibeError extends Error {
447
+ readonly code: string;
448
+ readonly hint?: string | undefined;
449
+ constructor(message: string, code: string, hint?: string | undefined);
450
+ }
451
+ declare class ScenarioNotFoundError extends VibeError {
452
+ constructor(name: string);
453
+ }
454
+ declare class ScenarioValidationError extends VibeError {
455
+ readonly issues: string[];
456
+ constructor(message: string, issues: string[]);
457
+ }
458
+ declare class AIToolNotFoundError extends VibeError {
459
+ static readonly installHints: Record<string, string>;
460
+ constructor(tool: string);
461
+ }
462
+ declare class SessionNotFoundError extends VibeError {
463
+ constructor(id: string);
464
+ }
465
+ declare class GitCloneError extends VibeError {
466
+ constructor(repo: string, reason?: string);
467
+ }
468
+ declare class SetupError extends VibeError {
469
+ constructor(command: string, reason?: string);
470
+ }
471
+
472
+ export { type AIRules, type AIToolLauncher, AIToolNotFoundError, ClaudeCodeLauncher, type DetectedTool, type Evaluation, GitCloneError, type LaunchConfig, type LaunchedProcess, type ProgressCallback, type RecordingData, type ScenarioConfig, ScenarioConfigSchema, type ScenarioInfo, ScenarioNotFoundError, ScenarioValidationError, type Session, type SessionEvent, type SessionEventType, SessionManager, SessionNotFoundError, SessionRecorder, SetupError, type StoredSession, type ValidationResult, VibeError, deleteSession, detectInstalledTools, discoverAllScenarios, discoverBuiltInScenarios, generateSystemPrompt, getAllLaunchers, getLauncher, importRepo, listActiveSessions, listSessions, loadScenarioConfig, loadSession, saveSession, toStoredSession, validateScenario, validateScenarioOrThrow };
package/dist/index.js ADDED
@@ -0,0 +1,642 @@
1
+ // src/scenario/types.ts
2
+ import { z } from "zod";
3
+ var AIRulesSchema = z.object({
4
+ /** Role description for the AI assistant */
5
+ role: z.string(),
6
+ /** Behavioral rules (e.g., "don't reveal the answer") */
7
+ rules: z.array(z.string()),
8
+ /** Knowledge about the bug/solution (hidden from candidate) */
9
+ knowledge: z.string()
10
+ });
11
+ var EvaluationSchema = z.object({
12
+ /** Evaluation criteria for the interviewer */
13
+ criteria: z.array(z.string()),
14
+ /** Description of the expected fix */
15
+ expected_fix: z.string().optional()
16
+ });
17
+ var PatchSchema = z.object({
18
+ /** Path to the file relative to repo root */
19
+ file: z.string(),
20
+ /** The original text to find */
21
+ find: z.string(),
22
+ /** The replacement text (with the bug) */
23
+ replace: z.string()
24
+ });
25
+ var ScenarioConfigSchema = z.object({
26
+ /** Scenario display name */
27
+ name: z.string(),
28
+ /** One-line description */
29
+ description: z.string(),
30
+ /** Difficulty level */
31
+ difficulty: z.enum(["easy", "medium", "hard"]),
32
+ /** Estimated time (e.g., "30-45m") */
33
+ estimated_time: z.string(),
34
+ /** Searchable tags */
35
+ tags: z.array(z.string()).default([]),
36
+ /** GitHub repo URL or owner/repo shorthand */
37
+ repo: z.string(),
38
+ /** Commit SHA to pin the clone to (ensures reproducibility) */
39
+ commit: z.string(),
40
+ /** Shell commands to run after cloning (e.g., ["npm install"]) */
41
+ setup: z.array(z.string()).default([]),
42
+ /** Find-and-replace patches to inject the bug after cloning */
43
+ patch: z.array(PatchSchema).default([]),
44
+ /** Briefing shown to the candidate (written like a team lead message) */
45
+ briefing: z.string(),
46
+ /** AI behavioral rules (injected via system prompt, hidden from candidate) */
47
+ ai_rules: AIRulesSchema,
48
+ /** Interviewer reference — what the fix looks like */
49
+ solution: z.string(),
50
+ /** Evaluation rubric */
51
+ evaluation: EvaluationSchema.optional(),
52
+ /** License of the original project */
53
+ license: z.string().optional()
54
+ });
55
+
56
+ // src/scenario/loader.ts
57
+ import { readFile } from "fs/promises";
58
+ import { parse as parseYaml } from "yaml";
59
+
60
+ // src/errors.ts
61
+ var VibeError = class extends Error {
62
+ constructor(message, code, hint) {
63
+ super(message);
64
+ this.code = code;
65
+ this.hint = hint;
66
+ this.name = "VibeError";
67
+ }
68
+ };
69
+ var ScenarioNotFoundError = class extends VibeError {
70
+ constructor(name) {
71
+ super(
72
+ `Scenario not found: ${name}`,
73
+ "SCENARIO_NOT_FOUND",
74
+ "Run `vibe-interviewing list` to see available scenarios"
75
+ );
76
+ }
77
+ };
78
+ var ScenarioValidationError = class extends VibeError {
79
+ constructor(message, issues) {
80
+ super(`Invalid scenario config: ${message}`, "SCENARIO_VALIDATION_ERROR", issues.join("\n"));
81
+ this.issues = issues;
82
+ }
83
+ };
84
+ var AIToolNotFoundError = class _AIToolNotFoundError extends VibeError {
85
+ static installHints = {
86
+ "claude-code": "Install Claude Code: npm install -g @anthropic-ai/claude-code"
87
+ };
88
+ constructor(tool) {
89
+ super(
90
+ `${tool} is not installed`,
91
+ "AI_TOOL_NOT_FOUND",
92
+ _AIToolNotFoundError.installHints[tool] ?? `Install ${tool} and try again`
93
+ );
94
+ }
95
+ };
96
+ var SessionNotFoundError = class extends VibeError {
97
+ constructor(id) {
98
+ super(
99
+ `Session not found: ${id}`,
100
+ "SESSION_NOT_FOUND",
101
+ "Run `vibe-interviewing list` to see active sessions"
102
+ );
103
+ }
104
+ };
105
+ var GitCloneError = class extends VibeError {
106
+ constructor(repo, reason) {
107
+ super(
108
+ `Failed to clone repository: ${repo}${reason ? ` \u2014 ${reason}` : ""}`,
109
+ "GIT_CLONE_FAILED",
110
+ "Check the repo URL and your network connection"
111
+ );
112
+ }
113
+ };
114
+ var SetupError = class extends VibeError {
115
+ constructor(command, reason) {
116
+ super(
117
+ `Setup command failed: ${command}${reason ? ` \u2014 ${reason}` : ""}`,
118
+ "SETUP_FAILED",
119
+ "Check the scenario setup commands and try again"
120
+ );
121
+ }
122
+ };
123
+
124
+ // src/scenario/loader.ts
125
+ import { existsSync } from "fs";
126
+ async function loadScenarioConfig(configPath) {
127
+ if (!existsSync(configPath)) {
128
+ throw new ScenarioNotFoundError(configPath);
129
+ }
130
+ const raw = await readFile(configPath, "utf-8");
131
+ const parsed = parseYaml(raw);
132
+ const result = ScenarioConfigSchema.safeParse(parsed);
133
+ if (!result.success) {
134
+ const issues = result.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`);
135
+ throw new ScenarioValidationError("validation failed", issues);
136
+ }
137
+ return result.data;
138
+ }
139
+ function generateSystemPrompt(config) {
140
+ const lines = [];
141
+ lines.push(`# Interview Scenario: ${config.name}`);
142
+ lines.push("");
143
+ lines.push("## Your Role");
144
+ lines.push(config.ai_rules.role.trim());
145
+ lines.push("");
146
+ lines.push("## Rules");
147
+ for (const rule of config.ai_rules.rules) {
148
+ lines.push(`- ${rule}`);
149
+ }
150
+ lines.push("");
151
+ lines.push("## Knowledge (DO NOT share directly with the candidate)");
152
+ lines.push(config.ai_rules.knowledge.trim());
153
+ return lines.join("\n");
154
+ }
155
+
156
+ // src/scenario/validator.ts
157
+ async function validateScenario(config) {
158
+ const warnings = [];
159
+ const errors = [];
160
+ if (!config.briefing.trim()) {
161
+ errors.push("Briefing cannot be empty");
162
+ }
163
+ if (!config.ai_rules.role.trim()) {
164
+ errors.push("ai_rules.role cannot be empty");
165
+ }
166
+ if (!config.repo.trim()) {
167
+ errors.push("repo cannot be empty");
168
+ }
169
+ if (!config.commit.trim()) {
170
+ errors.push("commit cannot be empty \u2014 pin to a specific commit SHA for reproducibility");
171
+ }
172
+ if (config.ai_rules.rules.length === 0) {
173
+ warnings.push("ai_rules.rules is empty \u2014 the AI will have no behavioral constraints");
174
+ }
175
+ if (!config.solution.trim()) {
176
+ warnings.push("solution is empty \u2014 interviewers will have no solution reference");
177
+ }
178
+ if (!config.evaluation) {
179
+ warnings.push("No evaluation criteria defined");
180
+ }
181
+ return {
182
+ valid: errors.length === 0,
183
+ warnings,
184
+ errors
185
+ };
186
+ }
187
+ async function validateScenarioOrThrow(config) {
188
+ const result = await validateScenario(config);
189
+ if (!result.valid) {
190
+ throw new ScenarioValidationError("scenario validation failed", result.errors);
191
+ }
192
+ return result;
193
+ }
194
+
195
+ // src/scenario/registry.ts
196
+ import { readFile as readFile2 } from "fs/promises";
197
+ import { join } from "path";
198
+ import { existsSync as existsSync2 } from "fs";
199
+ import { parse as parseYaml2 } from "yaml";
200
+ async function getScenariosPackagePath() {
201
+ try {
202
+ const { getScenariosDir } = await import("@vibe-interviewing/scenarios");
203
+ return getScenariosDir();
204
+ } catch {
205
+ const fromCwd = join(process.cwd(), "packages", "scenarios");
206
+ if (existsSync2(fromCwd)) {
207
+ return fromCwd;
208
+ }
209
+ throw new Error("Could not locate @vibe-interviewing/scenarios package");
210
+ }
211
+ }
212
+ async function discoverBuiltInScenarios() {
213
+ const scenariosPath = await getScenariosPackagePath();
214
+ const registryPath = join(scenariosPath, "registry.yaml");
215
+ if (!existsSync2(registryPath)) {
216
+ return [];
217
+ }
218
+ const raw = await readFile2(registryPath, "utf-8");
219
+ const registry = parseYaml2(raw);
220
+ if (!registry.scenarios || !Array.isArray(registry.scenarios)) {
221
+ return [];
222
+ }
223
+ const scenarios = [];
224
+ for (const entry of registry.scenarios) {
225
+ const scenarioConfigPath = join(scenariosPath, entry.name, "scenario.yaml");
226
+ if (existsSync2(scenarioConfigPath)) {
227
+ const config = await loadScenarioConfig(scenarioConfigPath);
228
+ scenarios.push({
229
+ name: entry.name,
230
+ config,
231
+ builtIn: true
232
+ });
233
+ }
234
+ }
235
+ return scenarios;
236
+ }
237
+ async function discoverAllScenarios() {
238
+ return discoverBuiltInScenarios();
239
+ }
240
+
241
+ // src/scenario/importer.ts
242
+ import { mkdtemp } from "fs/promises";
243
+ import { join as join2 } from "path";
244
+ import { tmpdir } from "os";
245
+ async function importRepo(repoUrl, targetPath, ref) {
246
+ const { simpleGit } = await import("simple-git");
247
+ const url = normalizeRepoUrl(repoUrl);
248
+ const dest = targetPath ?? await mkdtemp(join2(tmpdir(), "vibe-import-"));
249
+ const git = simpleGit();
250
+ try {
251
+ if (ref && /^[0-9a-f]{7,40}$/i.test(ref)) {
252
+ await git.init([dest]);
253
+ const repoGit = simpleGit(dest);
254
+ await repoGit.addRemote("origin", url);
255
+ await repoGit.fetch(["origin", ref, "--depth", "1"]);
256
+ await repoGit.checkout(["FETCH_HEAD"]);
257
+ } else if (ref) {
258
+ await git.clone(url, dest, ["--depth", "1", "--branch", ref]);
259
+ } else {
260
+ await git.clone(url, dest, ["--depth", "1"]);
261
+ }
262
+ } catch (err) {
263
+ throw new GitCloneError(url, err instanceof Error ? err.message : String(err));
264
+ }
265
+ return dest;
266
+ }
267
+ function normalizeRepoUrl(input) {
268
+ if (input.startsWith("http://") || input.startsWith("https://")) {
269
+ return input;
270
+ }
271
+ if (input.startsWith("git@")) {
272
+ return input.replace("git@github.com:", "https://github.com/").replace(/\.git$/, "");
273
+ }
274
+ if (/^[a-zA-Z0-9_-]+\/[a-zA-Z0-9._-]+$/.test(input)) {
275
+ return `https://github.com/${input}`;
276
+ }
277
+ return input;
278
+ }
279
+
280
+ // src/launcher/claude-code.ts
281
+ import { spawn } from "child_process";
282
+ import { execFile } from "child_process";
283
+ import { promisify } from "util";
284
+ import { readFile as readFile4 } from "fs/promises";
285
+
286
+ // src/session/recorder.ts
287
+ import { readFile as readFile3, writeFile, readdir, mkdir } from "fs/promises";
288
+ import { join as join3 } from "path";
289
+ import { homedir } from "os";
290
+ import { existsSync as existsSync3 } from "fs";
291
+ var RECORDINGS_DIR = join3(homedir(), ".vibe-interviewing", "recordings");
292
+ async function ensureRecordingsDir() {
293
+ if (!existsSync3(RECORDINGS_DIR)) {
294
+ await mkdir(RECORDINGS_DIR, { recursive: true });
295
+ }
296
+ }
297
+ var SessionRecorder = class _SessionRecorder {
298
+ events = [];
299
+ startTime;
300
+ startedAt;
301
+ constructor() {
302
+ this.startTime = Date.now();
303
+ this.startedAt = (/* @__PURE__ */ new Date()).toISOString();
304
+ }
305
+ /** Record a timestamped event */
306
+ record(type, data) {
307
+ this.events.push({
308
+ timestamp: Date.now() - this.startTime,
309
+ type,
310
+ data
311
+ });
312
+ }
313
+ /** Get all recorded events */
314
+ getEvents() {
315
+ return this.events;
316
+ }
317
+ /** Serialize the recording to a JSON-compatible object */
318
+ toJSON(sessionId) {
319
+ return {
320
+ sessionId,
321
+ startedAt: this.startedAt,
322
+ events: [...this.events]
323
+ };
324
+ }
325
+ /** Create a SessionRecorder pre-populated with events from serialized data */
326
+ static fromJSON(data) {
327
+ const recorder = new _SessionRecorder();
328
+ Object.defineProperty(recorder, "startedAt", { value: data.startedAt });
329
+ for (const event of data.events) {
330
+ recorder.events.push({ ...event });
331
+ }
332
+ return recorder;
333
+ }
334
+ /** Save the recording to disk */
335
+ async save(sessionId) {
336
+ await ensureRecordingsDir();
337
+ const filePath = join3(RECORDINGS_DIR, `${sessionId}.json`);
338
+ const data = this.toJSON(sessionId);
339
+ await writeFile(filePath, JSON.stringify(data, null, 2));
340
+ }
341
+ /** Load a recording from disk */
342
+ static async load(sessionId) {
343
+ const filePath = join3(RECORDINGS_DIR, `${sessionId}.json`);
344
+ const raw = await readFile3(filePath, "utf-8");
345
+ const data = JSON.parse(raw);
346
+ return _SessionRecorder.fromJSON(data);
347
+ }
348
+ /** List all available recording session IDs */
349
+ static async list() {
350
+ await ensureRecordingsDir();
351
+ const files = await readdir(RECORDINGS_DIR);
352
+ return files.filter((f) => f.endsWith(".json")).map((f) => f.replace(/\.json$/, "")).sort();
353
+ }
354
+ };
355
+
356
+ // src/launcher/claude-code.ts
357
+ var execFileAsync = promisify(execFile);
358
+ var ClaudeCodeLauncher = class {
359
+ name = "claude-code";
360
+ displayName = "Claude Code";
361
+ /** Check if the claude CLI is installed */
362
+ async isInstalled() {
363
+ try {
364
+ await execFileAsync("claude", ["--version"]);
365
+ return true;
366
+ } catch {
367
+ return false;
368
+ }
369
+ }
370
+ /** Get the installed Claude Code version */
371
+ async getVersion() {
372
+ try {
373
+ const { stdout } = await execFileAsync("claude", ["--version"]);
374
+ return stdout.trim();
375
+ } catch {
376
+ return null;
377
+ }
378
+ }
379
+ /** Launch Claude Code in the given working directory with the provided config */
380
+ async launch(workdir, config) {
381
+ const args = [];
382
+ const systemPrompt = await readFile4(config.systemPromptPath, "utf-8");
383
+ args.push("--append-system-prompt", systemPrompt);
384
+ args.push("--permission-mode", config.permissionMode ?? "default");
385
+ args.push("--name", `Interview: ${config.scenarioName}`);
386
+ if (config.model) {
387
+ args.push("--model", config.model);
388
+ }
389
+ if (config.disallowedTools && config.disallowedTools.length > 0) {
390
+ args.push("--disallowedTools", ...config.disallowedTools);
391
+ }
392
+ const useRecording = config.recording === true;
393
+ const recorder = useRecording ? new SessionRecorder() : void 0;
394
+ const proc = spawn("claude", args, {
395
+ cwd: workdir,
396
+ stdio: useRecording ? ["inherit", "pipe", "pipe"] : "inherit",
397
+ env: { ...process.env }
398
+ });
399
+ if (useRecording && recorder) {
400
+ proc.stdout?.on("data", (chunk) => {
401
+ const text = chunk.toString("utf-8");
402
+ recorder.record("stdout", text);
403
+ process.stdout.write(chunk);
404
+ });
405
+ proc.stderr?.on("data", (chunk) => {
406
+ const text = chunk.toString("utf-8");
407
+ recorder.record("stderr", text);
408
+ process.stderr.write(chunk);
409
+ });
410
+ }
411
+ return {
412
+ wait: () => new Promise((resolve) => {
413
+ proc.on("exit", (code) => resolve({ exitCode: code ?? 0 }));
414
+ }),
415
+ kill: async () => {
416
+ proc.kill("SIGTERM");
417
+ },
418
+ recorder
419
+ };
420
+ }
421
+ };
422
+
423
+ // src/launcher/detector.ts
424
+ var launchers = [new ClaudeCodeLauncher()];
425
+ async function detectInstalledTools() {
426
+ const results = [];
427
+ for (const launcher of launchers) {
428
+ const installed = await launcher.isInstalled();
429
+ if (installed) {
430
+ const version = await launcher.getVersion();
431
+ results.push({ launcher, version });
432
+ }
433
+ }
434
+ return results;
435
+ }
436
+ function getLauncher(name) {
437
+ return launchers.find((l) => l.name === name);
438
+ }
439
+ function getAllLaunchers() {
440
+ return [...launchers];
441
+ }
442
+
443
+ // src/session/manager.ts
444
+ import { execSync } from "child_process";
445
+ import { readFile as readFile6, writeFile as writeFile3, mkdir as mkdir3, rm } from "fs/promises";
446
+ import { join as join5 } from "path";
447
+ import { homedir as homedir3 } from "os";
448
+ import { randomBytes } from "crypto";
449
+
450
+ // src/session/store.ts
451
+ import { readFile as readFile5, writeFile as writeFile2, readdir as readdir2, unlink, mkdir as mkdir2 } from "fs/promises";
452
+ import { join as join4 } from "path";
453
+ import { homedir as homedir2 } from "os";
454
+ import { existsSync as existsSync4 } from "fs";
455
+ var SESSIONS_DIR = join4(homedir2(), ".vibe-interviewing", "sessions");
456
+ async function ensureSessionsDir() {
457
+ if (!existsSync4(SESSIONS_DIR)) {
458
+ await mkdir2(SESSIONS_DIR, { recursive: true });
459
+ }
460
+ }
461
+ async function saveSession(session) {
462
+ await ensureSessionsDir();
463
+ const filePath = join4(SESSIONS_DIR, `${session.id}.json`);
464
+ await writeFile2(filePath, JSON.stringify(session, null, 2));
465
+ }
466
+ async function loadSession(id) {
467
+ const filePath = join4(SESSIONS_DIR, `${id}.json`);
468
+ if (!existsSync4(filePath)) return null;
469
+ const raw = await readFile5(filePath, "utf-8");
470
+ return JSON.parse(raw);
471
+ }
472
+ async function deleteSession(id) {
473
+ const filePath = join4(SESSIONS_DIR, `${id}.json`);
474
+ if (existsSync4(filePath)) {
475
+ await unlink(filePath);
476
+ }
477
+ }
478
+ async function listSessions() {
479
+ await ensureSessionsDir();
480
+ const files = await readdir2(SESSIONS_DIR);
481
+ const sessions = [];
482
+ for (const file of files) {
483
+ if (!file.endsWith(".json")) continue;
484
+ try {
485
+ const raw = await readFile5(join4(SESSIONS_DIR, file), "utf-8");
486
+ sessions.push(JSON.parse(raw));
487
+ } catch {
488
+ }
489
+ }
490
+ return sessions.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
491
+ }
492
+ async function listActiveSessions() {
493
+ const all = await listSessions();
494
+ return all.filter((s) => s.status !== "complete");
495
+ }
496
+
497
+ // src/session/types.ts
498
+ function toStoredSession(session) {
499
+ return {
500
+ id: session.id,
501
+ scenarioName: session.scenarioName,
502
+ status: session.status,
503
+ workdir: session.workdir,
504
+ systemPromptPath: session.systemPromptPath,
505
+ aiTool: session.aiTool,
506
+ createdAt: session.createdAt,
507
+ startedAt: session.startedAt,
508
+ completedAt: session.completedAt
509
+ };
510
+ }
511
+
512
+ // src/session/manager.ts
513
+ var SessionManager = class {
514
+ constructor(launcher) {
515
+ this.launcher = launcher;
516
+ }
517
+ /**
518
+ * Create a new interview session.
519
+ *
520
+ * Flow:
521
+ * 1. Clone the repo at a pinned commit
522
+ * 2. Apply bug patches (find/replace in source files)
523
+ * 3. Wipe git history so the candidate can't diff to find the bug
524
+ * 4. Remove scenario.yaml from workspace (interviewer-only)
525
+ * 5. Write BRIEFING.md and system prompt
526
+ * 6. Run setup commands (npm install, etc.)
527
+ */
528
+ async createSession(config, workdir, onProgress) {
529
+ const id = randomBytes(4).toString("hex");
530
+ const sessionDir = workdir ?? join5(homedir3(), "vibe-sessions", `${config.name}-${id}`);
531
+ const session = {
532
+ id,
533
+ scenarioName: config.name,
534
+ workdir: sessionDir,
535
+ systemPromptPath: "",
536
+ status: "cloning",
537
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
538
+ };
539
+ onProgress?.("Cloning repository...");
540
+ await importRepo(config.repo, sessionDir, config.commit);
541
+ onProgress?.("Injecting scenario...");
542
+ for (const p of config.patch) {
543
+ const filePath = join5(sessionDir, p.file);
544
+ const content = await readFile6(filePath, "utf-8");
545
+ const patched = content.replace(p.find, p.replace);
546
+ if (patched === content) {
547
+ throw new SetupError(
548
+ `patch ${p.file}`,
549
+ `Could not find text to replace. The upstream code may have changed.`
550
+ );
551
+ }
552
+ await writeFile3(filePath, patched);
553
+ }
554
+ onProgress?.("Preparing workspace...");
555
+ await rm(join5(sessionDir, ".git"), { recursive: true, force: true });
556
+ execSync('git init && git add -A && git commit -m "initial"', {
557
+ cwd: sessionDir,
558
+ stdio: "ignore"
559
+ });
560
+ await rm(join5(sessionDir, "scenario.yaml"), { force: true });
561
+ await writeFile3(join5(sessionDir, "BRIEFING.md"), `# Interview Briefing
562
+
563
+ ${config.briefing}`);
564
+ const promptDir = join5(homedir3(), ".vibe-interviewing", "prompts");
565
+ await mkdir3(promptDir, { recursive: true });
566
+ const systemPromptPath = join5(promptDir, `${id}.md`);
567
+ await writeFile3(systemPromptPath, generateSystemPrompt(config));
568
+ session.systemPromptPath = systemPromptPath;
569
+ session.status = "setting-up";
570
+ for (const cmd of config.setup) {
571
+ onProgress?.(`Running: ${cmd}`);
572
+ try {
573
+ execSync(cmd, { cwd: sessionDir, stdio: "pipe", timeout: 3e5 });
574
+ } catch (err) {
575
+ throw new SetupError(cmd, err instanceof Error ? err.message : String(err));
576
+ }
577
+ }
578
+ session.status = "running";
579
+ await saveSession(toStoredSession(session));
580
+ return { session, config };
581
+ }
582
+ /** Launch the AI coding tool for an active session */
583
+ async launchAITool(session, _config, launchConfig = {}) {
584
+ const fullConfig = {
585
+ scenarioName: session.scenarioName,
586
+ systemPromptPath: session.systemPromptPath,
587
+ ...launchConfig
588
+ };
589
+ session.aiTool = this.launcher.name;
590
+ session.startedAt = (/* @__PURE__ */ new Date()).toISOString();
591
+ await saveSession(toStoredSession(session));
592
+ const proc = await this.launcher.launch(session.workdir, fullConfig);
593
+ const result = await proc.wait();
594
+ session.status = "complete";
595
+ session.completedAt = (/* @__PURE__ */ new Date()).toISOString();
596
+ await saveSession(toStoredSession(session));
597
+ return result;
598
+ }
599
+ /** Destroy a session by removing its stored data */
600
+ async destroySession(session) {
601
+ await deleteSession(session.id);
602
+ }
603
+ /** Get elapsed time since the AI tool was launched, formatted as a human-readable string */
604
+ getElapsedTime(session) {
605
+ if (!session.startedAt) return null;
606
+ const elapsed = Date.now() - new Date(session.startedAt).getTime();
607
+ const minutes = Math.floor(elapsed / 6e4);
608
+ const seconds = Math.floor(elapsed % 6e4 / 1e3);
609
+ if (minutes === 0) return `${seconds}s`;
610
+ return `${minutes}m ${seconds}s`;
611
+ }
612
+ };
613
+ export {
614
+ AIToolNotFoundError,
615
+ ClaudeCodeLauncher,
616
+ GitCloneError,
617
+ ScenarioConfigSchema,
618
+ ScenarioNotFoundError,
619
+ ScenarioValidationError,
620
+ SessionManager,
621
+ SessionNotFoundError,
622
+ SessionRecorder,
623
+ SetupError,
624
+ VibeError,
625
+ deleteSession,
626
+ detectInstalledTools,
627
+ discoverAllScenarios,
628
+ discoverBuiltInScenarios,
629
+ generateSystemPrompt,
630
+ getAllLaunchers,
631
+ getLauncher,
632
+ importRepo,
633
+ listActiveSessions,
634
+ listSessions,
635
+ loadScenarioConfig,
636
+ loadSession,
637
+ saveSession,
638
+ toStoredSession,
639
+ validateScenario,
640
+ validateScenarioOrThrow
641
+ };
642
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/scenario/types.ts","../src/scenario/loader.ts","../src/errors.ts","../src/scenario/validator.ts","../src/scenario/registry.ts","../src/scenario/importer.ts","../src/launcher/claude-code.ts","../src/session/recorder.ts","../src/launcher/detector.ts","../src/session/manager.ts","../src/session/store.ts","../src/session/types.ts"],"sourcesContent":["import { z } from 'zod'\n\nconst AIRulesSchema = z.object({\n /** Role description for the AI assistant */\n role: z.string(),\n /** Behavioral rules (e.g., \"don't reveal the answer\") */\n rules: z.array(z.string()),\n /** Knowledge about the bug/solution (hidden from candidate) */\n knowledge: z.string(),\n})\n\nconst EvaluationSchema = z.object({\n /** Evaluation criteria for the interviewer */\n criteria: z.array(z.string()),\n /** Description of the expected fix */\n expected_fix: z.string().optional(),\n})\n\n/** A file modification to inject the bug */\nconst PatchSchema = z.object({\n /** Path to the file relative to repo root */\n file: z.string(),\n /** The original text to find */\n find: z.string(),\n /** The replacement text (with the bug) */\n replace: z.string(),\n})\n\n/** Full scenario configuration schema */\nexport const ScenarioConfigSchema = z.object({\n /** Scenario display name */\n name: z.string(),\n /** One-line description */\n description: z.string(),\n /** Difficulty level */\n difficulty: z.enum(['easy', 'medium', 'hard']),\n /** Estimated time (e.g., \"30-45m\") */\n estimated_time: z.string(),\n /** Searchable tags */\n tags: z.array(z.string()).default([]),\n\n /** GitHub repo URL or owner/repo shorthand */\n repo: z.string(),\n /** Commit SHA to pin the clone to (ensures reproducibility) */\n commit: z.string(),\n /** Shell commands to run after cloning (e.g., [\"npm install\"]) */\n setup: z.array(z.string()).default([]),\n\n /** Find-and-replace patches to inject the bug after cloning */\n patch: z.array(PatchSchema).default([]),\n\n /** Briefing shown to the candidate (written like a team lead message) */\n briefing: z.string(),\n /** AI behavioral rules (injected via system prompt, hidden from candidate) */\n ai_rules: AIRulesSchema,\n /** Interviewer reference — what the fix looks like */\n solution: z.string(),\n\n /** Evaluation rubric */\n evaluation: EvaluationSchema.optional(),\n /** License of the original project */\n license: z.string().optional(),\n})\n\nexport type ScenarioConfig = z.infer<typeof ScenarioConfigSchema>\nexport type AIRules = z.infer<typeof AIRulesSchema>\nexport type Evaluation = z.infer<typeof EvaluationSchema>\n\n/** Metadata about a discovered scenario */\nexport interface ScenarioInfo {\n /** Scenario name */\n name: string\n /** Parsed config */\n config: ScenarioConfig\n /** Whether this is a built-in scenario */\n builtIn: boolean\n}\n","import { readFile } from 'node:fs/promises'\nimport { parse as parseYaml } from 'yaml'\nimport { ScenarioNotFoundError, ScenarioValidationError } from '../errors.js'\nimport { ScenarioConfigSchema, type ScenarioConfig } from './types.js'\nimport { existsSync } from 'node:fs'\n\n/**\n * Load and parse a scenario config from a YAML file.\n *\n * @param configPath - Absolute path to the scenario.yaml file\n * @returns The parsed and validated scenario config\n */\nexport async function loadScenarioConfig(configPath: string): Promise<ScenarioConfig> {\n if (!existsSync(configPath)) {\n throw new ScenarioNotFoundError(configPath)\n }\n\n const raw = await readFile(configPath, 'utf-8')\n const parsed: unknown = parseYaml(raw)\n\n const result = ScenarioConfigSchema.safeParse(parsed)\n if (!result.success) {\n const issues = result.error.issues.map((i) => ` ${i.path.join('.')}: ${i.message}`)\n throw new ScenarioValidationError('validation failed', issues)\n }\n\n return result.data\n}\n\n/**\n * Generate a system prompt string from a scenario's ai_rules.\n *\n * This prompt is injected into the AI tool via --append-system-prompt\n * and is hidden from the candidate.\n *\n * @param config - The scenario config containing ai_rules\n * @returns The formatted system prompt\n */\nexport function generateSystemPrompt(config: ScenarioConfig): string {\n const lines: string[] = []\n\n lines.push(`# Interview Scenario: ${config.name}`)\n lines.push('')\n lines.push('## Your Role')\n lines.push(config.ai_rules.role.trim())\n lines.push('')\n lines.push('## Rules')\n for (const rule of config.ai_rules.rules) {\n lines.push(`- ${rule}`)\n }\n lines.push('')\n lines.push('## Knowledge (DO NOT share directly with the candidate)')\n lines.push(config.ai_rules.knowledge.trim())\n\n return lines.join('\\n')\n}\n","/** Base error class for all vibe-interviewing errors */\nexport class VibeError extends Error {\n constructor(\n message: string,\n public readonly code: string,\n public readonly hint?: string,\n ) {\n super(message)\n this.name = 'VibeError'\n }\n}\n\nexport class ScenarioNotFoundError extends VibeError {\n constructor(name: string) {\n super(\n `Scenario not found: ${name}`,\n 'SCENARIO_NOT_FOUND',\n 'Run `vibe-interviewing list` to see available scenarios',\n )\n }\n}\n\nexport class ScenarioValidationError extends VibeError {\n constructor(\n message: string,\n public readonly issues: string[],\n ) {\n super(`Invalid scenario config: ${message}`, 'SCENARIO_VALIDATION_ERROR', issues.join('\\n'))\n }\n}\n\nexport class AIToolNotFoundError extends VibeError {\n static readonly installHints: Record<string, string> = {\n 'claude-code': 'Install Claude Code: npm install -g @anthropic-ai/claude-code',\n }\n\n constructor(tool: string) {\n super(\n `${tool} is not installed`,\n 'AI_TOOL_NOT_FOUND',\n AIToolNotFoundError.installHints[tool] ?? `Install ${tool} and try again`,\n )\n }\n}\n\nexport class SessionNotFoundError extends VibeError {\n constructor(id: string) {\n super(\n `Session not found: ${id}`,\n 'SESSION_NOT_FOUND',\n 'Run `vibe-interviewing list` to see active sessions',\n )\n }\n}\n\nexport class GitCloneError extends VibeError {\n constructor(repo: string, reason?: string) {\n super(\n `Failed to clone repository: ${repo}${reason ? ` — ${reason}` : ''}`,\n 'GIT_CLONE_FAILED',\n 'Check the repo URL and your network connection',\n )\n }\n}\n\nexport class SetupError extends VibeError {\n constructor(command: string, reason?: string) {\n super(\n `Setup command failed: ${command}${reason ? ` — ${reason}` : ''}`,\n 'SETUP_FAILED',\n 'Check the scenario setup commands and try again',\n )\n }\n}\n","import { ScenarioValidationError } from '../errors.js'\nimport type { ScenarioConfig } from './types.js'\n\n/** Result of validating a scenario configuration */\nexport interface ValidationResult {\n /** Whether the scenario is valid (no errors) */\n valid: boolean\n /** Non-fatal issues that should be addressed */\n warnings: string[]\n /** Fatal issues that prevent the scenario from running */\n errors: string[]\n}\n\n/**\n * Validate a scenario config for completeness and correctness.\n *\n * @param config - The scenario config to validate\n * @returns Validation result with errors and warnings\n */\nexport async function validateScenario(config: ScenarioConfig): Promise<ValidationResult> {\n const warnings: string[] = []\n const errors: string[] = []\n\n // Check briefing is not empty\n if (!config.briefing.trim()) {\n errors.push('Briefing cannot be empty')\n }\n\n // Check AI rules\n if (!config.ai_rules.role.trim()) {\n errors.push('ai_rules.role cannot be empty')\n }\n\n // Check repo is not empty\n if (!config.repo.trim()) {\n errors.push('repo cannot be empty')\n }\n\n // Check commit is pinned\n if (!config.commit.trim()) {\n errors.push('commit cannot be empty — pin to a specific commit SHA for reproducibility')\n }\n\n // Warnings for non-critical missing content\n if (config.ai_rules.rules.length === 0) {\n warnings.push('ai_rules.rules is empty — the AI will have no behavioral constraints')\n }\n\n if (!config.solution.trim()) {\n warnings.push('solution is empty — interviewers will have no solution reference')\n }\n\n if (!config.evaluation) {\n warnings.push('No evaluation criteria defined')\n }\n\n return {\n valid: errors.length === 0,\n warnings,\n errors,\n }\n}\n\n/**\n * Validate a scenario config and throw if invalid.\n *\n * @param config - The scenario config to validate\n * @returns Validation result (only returned if valid)\n * @throws ScenarioValidationError if the config has errors\n */\nexport async function validateScenarioOrThrow(config: ScenarioConfig): Promise<ValidationResult> {\n const result = await validateScenario(config)\n if (!result.valid) {\n throw new ScenarioValidationError('scenario validation failed', result.errors)\n }\n return result\n}\n","import { readFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { existsSync } from 'node:fs'\nimport { parse as parseYaml } from 'yaml'\nimport { loadScenarioConfig } from './loader.js'\nimport type { ScenarioInfo } from './types.js'\n\n/** Shape of a single entry in registry.yaml */\ninterface RegistryEntry {\n name: string\n repo: string\n commit: string\n description: string\n difficulty: 'easy' | 'medium' | 'hard'\n estimated_time: string\n}\n\n/** Shape of the registry.yaml file */\ninterface RegistryFile {\n scenarios: RegistryEntry[]\n}\n\n/**\n * Get the path to the scenarios package directory.\n *\n * Resolves via the @vibe-interviewing/scenarios package, which works both\n * in the monorepo (workspace link) and when installed from npm.\n */\nasync function getScenariosPackagePath(): Promise<string> {\n try {\n const { getScenariosDir } = await import('@vibe-interviewing/scenarios')\n return getScenariosDir()\n } catch {\n // Fallback: try relative to process.cwd() (monorepo root)\n const fromCwd = join(process.cwd(), 'packages', 'scenarios')\n if (existsSync(fromCwd)) {\n return fromCwd\n }\n throw new Error('Could not locate @vibe-interviewing/scenarios package')\n }\n}\n\n/**\n * Discover built-in scenarios from the registry.yaml file in the scenarios package.\n *\n * Each entry in the registry points to a scenario directory containing a full\n * scenario.yaml. The registry provides quick lookup metadata, while the full\n * config is loaded from the scenario's own config file.\n *\n * @returns Array of discovered scenario info objects\n */\nexport async function discoverBuiltInScenarios(): Promise<ScenarioInfo[]> {\n const scenariosPath = await getScenariosPackagePath()\n const registryPath = join(scenariosPath, 'registry.yaml')\n\n if (!existsSync(registryPath)) {\n return []\n }\n\n const raw = await readFile(registryPath, 'utf-8')\n const registry = parseYaml(raw) as RegistryFile\n\n if (!registry.scenarios || !Array.isArray(registry.scenarios)) {\n return []\n }\n\n const scenarios: ScenarioInfo[] = []\n\n for (const entry of registry.scenarios) {\n // Load the full config from the scenario's own directory\n const scenarioConfigPath = join(scenariosPath, entry.name, 'scenario.yaml')\n\n if (existsSync(scenarioConfigPath)) {\n const config = await loadScenarioConfig(scenarioConfigPath)\n scenarios.push({\n name: entry.name,\n config,\n builtIn: true,\n })\n }\n }\n\n return scenarios\n}\n\n/**\n * Discover all available scenarios.\n *\n * Currently returns only built-in scenarios from the registry.\n * Local scenario support can be added later.\n *\n * @returns Array of all discovered scenario info objects\n */\nexport async function discoverAllScenarios(): Promise<ScenarioInfo[]> {\n return discoverBuiltInScenarios()\n}\n","import { mkdtemp } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { GitCloneError } from '../errors.js'\n\n/**\n * Import a repository by cloning it to a local directory.\n *\n * For commit SHAs, uses a shallow fetch to avoid downloading full history.\n *\n * @param repoUrl - GitHub URL, SSH URL, or owner/repo shorthand\n * @param targetPath - Directory to clone into (defaults to a temp directory)\n * @param ref - Optional branch, tag, or commit to checkout\n * @returns The path to the cloned directory\n */\nexport async function importRepo(repoUrl: string, targetPath?: string, ref?: string): Promise<string> {\n const { simpleGit } = await import('simple-git')\n\n // Normalize shorthand (owner/repo -> https://github.com/owner/repo)\n const url = normalizeRepoUrl(repoUrl)\n\n // Clone to target path or temp directory\n const dest = targetPath ?? (await mkdtemp(join(tmpdir(), 'vibe-import-')))\n\n const git = simpleGit()\n\n try {\n if (ref && /^[0-9a-f]{7,40}$/i.test(ref)) {\n // Commit SHA — init + shallow fetch to avoid downloading full history\n await git.init([dest])\n const repoGit = simpleGit(dest)\n await repoGit.addRemote('origin', url)\n await repoGit.fetch(['origin', ref, '--depth', '1'])\n await repoGit.checkout(['FETCH_HEAD'])\n } else if (ref) {\n // Branch or tag — shallow clone\n await git.clone(url, dest, ['--depth', '1', '--branch', ref])\n } else {\n await git.clone(url, dest, ['--depth', '1'])\n }\n } catch (err) {\n throw new GitCloneError(url, err instanceof Error ? err.message : String(err))\n }\n\n return dest\n}\n\n/** Normalize various repo URL formats to full HTTPS URLs */\nfunction normalizeRepoUrl(input: string): string {\n // Already a full URL\n if (input.startsWith('http://') || input.startsWith('https://')) {\n return input\n }\n\n // Git SSH format\n if (input.startsWith('git@')) {\n return input.replace('git@github.com:', 'https://github.com/').replace(/\\.git$/, '')\n }\n\n // Shorthand: owner/repo\n if (/^[a-zA-Z0-9_-]+\\/[a-zA-Z0-9._-]+$/.test(input)) {\n return `https://github.com/${input}`\n }\n\n return input\n}\n","import { spawn } from 'node:child_process'\nimport { execFile } from 'node:child_process'\nimport { promisify } from 'node:util'\nimport { readFile } from 'node:fs/promises'\nimport type { AIToolLauncher, LaunchConfig, LaunchedProcess } from './types.js'\nimport { SessionRecorder } from '../session/recorder.js'\n\nconst execFileAsync = promisify(execFile)\n\n/** Launcher for Anthropic's Claude Code CLI */\nexport class ClaudeCodeLauncher implements AIToolLauncher {\n readonly name = 'claude-code'\n readonly displayName = 'Claude Code'\n\n /** Check if the claude CLI is installed */\n async isInstalled(): Promise<boolean> {\n try {\n await execFileAsync('claude', ['--version'])\n return true\n } catch {\n return false\n }\n }\n\n /** Get the installed Claude Code version */\n async getVersion(): Promise<string | null> {\n try {\n const { stdout } = await execFileAsync('claude', ['--version'])\n return stdout.trim()\n } catch {\n return null\n }\n }\n\n /** Launch Claude Code in the given working directory with the provided config */\n async launch(workdir: string, config: LaunchConfig): Promise<LaunchedProcess> {\n const args: string[] = []\n\n // Inject hidden system prompt\n const systemPrompt = await readFile(config.systemPromptPath, 'utf-8')\n args.push('--append-system-prompt', systemPrompt)\n\n // Set permission mode\n args.push('--permission-mode', config.permissionMode ?? 'default')\n\n // Set session name\n args.push('--name', `Interview: ${config.scenarioName}`)\n\n // Set model if specified\n if (config.model) {\n args.push('--model', config.model)\n }\n\n // Disallow tools for fairness\n if (config.disallowedTools && config.disallowedTools.length > 0) {\n args.push('--disallowedTools', ...config.disallowedTools)\n }\n\n // When recording, pipe stdout/stderr through the recorder\n const useRecording = config.recording === true\n const recorder = useRecording ? new SessionRecorder() : undefined\n\n // Spawn claude process\n const proc = spawn('claude', args, {\n cwd: workdir,\n stdio: useRecording ? ['inherit', 'pipe', 'pipe'] : 'inherit',\n env: { ...process.env },\n })\n\n // Forward piped output to the terminal and record it\n if (useRecording && recorder) {\n proc.stdout?.on('data', (chunk: Buffer) => {\n const text = chunk.toString('utf-8')\n recorder.record('stdout', text)\n process.stdout.write(chunk)\n })\n\n proc.stderr?.on('data', (chunk: Buffer) => {\n const text = chunk.toString('utf-8')\n recorder.record('stderr', text)\n process.stderr.write(chunk)\n })\n }\n\n return {\n wait: () =>\n new Promise((resolve) => {\n proc.on('exit', (code) => resolve({ exitCode: code ?? 0 }))\n }),\n kill: async () => {\n proc.kill('SIGTERM')\n },\n recorder,\n }\n }\n}\n","import { readFile, writeFile, readdir, mkdir } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { homedir } from 'node:os'\nimport { existsSync } from 'node:fs'\n\n/** Event types that can be captured during a session */\nexport type SessionEventType = 'stdout' | 'stderr' | 'command' | 'note'\n\n/** A single timestamped event captured during a session */\nexport interface SessionEvent {\n /** Milliseconds since recording started */\n timestamp: number\n /** The kind of event */\n type: SessionEventType\n /** The captured data */\n data: string\n}\n\n/** Serialized recording format */\nexport interface RecordingData {\n /** Session ID this recording belongs to */\n sessionId: string\n /** ISO string of when recording started */\n startedAt: string\n /** All captured events */\n events: SessionEvent[]\n}\n\nconst RECORDINGS_DIR = join(homedir(), '.vibe-interviewing', 'recordings')\n\nasync function ensureRecordingsDir(): Promise<void> {\n if (!existsSync(RECORDINGS_DIR)) {\n await mkdir(RECORDINGS_DIR, { recursive: true })\n }\n}\n\n/**\n * Records timestamped events during an interview session.\n *\n * Captures stdout, stderr, commands, and notes with millisecond timestamps\n * relative to when the recorder was created.\n */\nexport class SessionRecorder {\n private readonly events: SessionEvent[] = []\n private readonly startTime: number\n private readonly startedAt: string\n\n constructor() {\n this.startTime = Date.now()\n this.startedAt = new Date().toISOString()\n }\n\n /** Record a timestamped event */\n record(type: SessionEventType, data: string): void {\n this.events.push({\n timestamp: Date.now() - this.startTime,\n type,\n data,\n })\n }\n\n /** Get all recorded events */\n getEvents(): ReadonlyArray<SessionEvent> {\n return this.events\n }\n\n /** Serialize the recording to a JSON-compatible object */\n toJSON(sessionId: string): RecordingData {\n return {\n sessionId,\n startedAt: this.startedAt,\n events: [...this.events],\n }\n }\n\n /** Create a SessionRecorder pre-populated with events from serialized data */\n static fromJSON(data: RecordingData): SessionRecorder {\n const recorder = new SessionRecorder()\n // Override the startedAt via Object.defineProperty since it's readonly\n Object.defineProperty(recorder, 'startedAt', { value: data.startedAt })\n for (const event of data.events) {\n recorder.events.push({ ...event })\n }\n return recorder\n }\n\n /** Save the recording to disk */\n async save(sessionId: string): Promise<void> {\n await ensureRecordingsDir()\n const filePath = join(RECORDINGS_DIR, `${sessionId}.json`)\n const data = this.toJSON(sessionId)\n await writeFile(filePath, JSON.stringify(data, null, 2))\n }\n\n /** Load a recording from disk */\n static async load(sessionId: string): Promise<SessionRecorder> {\n const filePath = join(RECORDINGS_DIR, `${sessionId}.json`)\n const raw = await readFile(filePath, 'utf-8')\n const data = JSON.parse(raw) as RecordingData\n return SessionRecorder.fromJSON(data)\n }\n\n /** List all available recording session IDs */\n static async list(): Promise<string[]> {\n await ensureRecordingsDir()\n const files = await readdir(RECORDINGS_DIR)\n return files\n .filter((f) => f.endsWith('.json'))\n .map((f) => f.replace(/\\.json$/, ''))\n .sort()\n }\n}\n","import { ClaudeCodeLauncher } from './claude-code.js'\nimport type { AIToolLauncher } from './types.js'\n\n/** All supported AI tool launchers */\nconst launchers: AIToolLauncher[] = [new ClaudeCodeLauncher()]\n\n/** Information about a detected AI coding tool */\nexport interface DetectedTool {\n /** The launcher instance */\n launcher: AIToolLauncher\n /** Installed version string, or null if unknown */\n version: string | null\n}\n\n/** Detect which AI coding tools are installed on the system */\nexport async function detectInstalledTools(): Promise<DetectedTool[]> {\n const results: DetectedTool[] = []\n\n for (const launcher of launchers) {\n const installed = await launcher.isInstalled()\n if (installed) {\n const version = await launcher.getVersion()\n results.push({ launcher, version })\n }\n }\n\n return results\n}\n\n/** Get a launcher by its internal name */\nexport function getLauncher(name: string): AIToolLauncher | undefined {\n return launchers.find((l) => l.name === name)\n}\n\n/** Get all registered launchers */\nexport function getAllLaunchers(): AIToolLauncher[] {\n return [...launchers]\n}\n","import { execSync } from 'node:child_process'\nimport { readFile, writeFile, mkdir, rm } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { homedir } from 'node:os'\nimport { randomBytes } from 'node:crypto'\nimport type { AIToolLauncher, LaunchConfig } from '../launcher/types.js'\nimport type { ScenarioConfig } from '../scenario/types.js'\nimport { importRepo } from '../scenario/importer.js'\nimport { generateSystemPrompt } from '../scenario/loader.js'\nimport { SetupError } from '../errors.js'\nimport { saveSession, deleteSession } from './store.js'\nimport { toStoredSession } from './types.js'\nimport type { Session } from './types.js'\n\nexport type { Session }\n\n/** Callback for reporting session progress */\nexport type ProgressCallback = (stage: string) => void\n\n/** Manages the lifecycle of an interview session */\nexport class SessionManager {\n constructor(private launcher: AIToolLauncher) {}\n\n /**\n * Create a new interview session.\n *\n * Flow:\n * 1. Clone the repo at a pinned commit\n * 2. Apply bug patches (find/replace in source files)\n * 3. Wipe git history so the candidate can't diff to find the bug\n * 4. Remove scenario.yaml from workspace (interviewer-only)\n * 5. Write BRIEFING.md and system prompt\n * 6. Run setup commands (npm install, etc.)\n */\n async createSession(\n config: ScenarioConfig,\n workdir?: string,\n onProgress?: ProgressCallback,\n ): Promise<{ session: Session; config: ScenarioConfig }> {\n const id = randomBytes(4).toString('hex')\n const sessionDir = workdir ?? join(homedir(), 'vibe-sessions', `${config.name}-${id}`)\n\n const session: Session = {\n id,\n scenarioName: config.name,\n workdir: sessionDir,\n systemPromptPath: '',\n status: 'cloning',\n createdAt: new Date().toISOString(),\n }\n\n // 1. Clone the repo at the pinned commit\n onProgress?.('Cloning repository...')\n await importRepo(config.repo, sessionDir, config.commit)\n\n // 2. Apply bug patches\n onProgress?.('Injecting scenario...')\n for (const p of config.patch) {\n const filePath = join(sessionDir, p.file)\n const content = await readFile(filePath, 'utf-8')\n const patched = content.replace(p.find, p.replace)\n if (patched === content) {\n throw new SetupError(\n `patch ${p.file}`,\n `Could not find text to replace. The upstream code may have changed.`,\n )\n }\n await writeFile(filePath, patched)\n }\n\n // 3. Wipe git history so candidate can't see the injected changes\n onProgress?.('Preparing workspace...')\n await rm(join(sessionDir, '.git'), { recursive: true, force: true })\n execSync('git init && git add -A && git commit -m \"initial\"', {\n cwd: sessionDir,\n stdio: 'ignore',\n })\n\n // 4. Remove scenario.yaml from workspace (interviewer-only data)\n await rm(join(sessionDir, 'scenario.yaml'), { force: true })\n\n // 5. Write BRIEFING.md\n await writeFile(join(sessionDir, 'BRIEFING.md'), `# Interview Briefing\\n\\n${config.briefing}`)\n\n // Write system prompt OUTSIDE the workspace\n const promptDir = join(homedir(), '.vibe-interviewing', 'prompts')\n await mkdir(promptDir, { recursive: true })\n const systemPromptPath = join(promptDir, `${id}.md`)\n await writeFile(systemPromptPath, generateSystemPrompt(config))\n session.systemPromptPath = systemPromptPath\n\n // 6. Run setup commands\n session.status = 'setting-up'\n for (const cmd of config.setup) {\n onProgress?.(`Running: ${cmd}`)\n try {\n execSync(cmd, { cwd: sessionDir, stdio: 'pipe', timeout: 300000 })\n } catch (err) {\n throw new SetupError(cmd, err instanceof Error ? err.message : String(err))\n }\n }\n\n session.status = 'running'\n await saveSession(toStoredSession(session))\n return { session, config }\n }\n\n /** Launch the AI coding tool for an active session */\n async launchAITool(\n session: Session,\n _config: ScenarioConfig,\n launchConfig: Partial<LaunchConfig> = {},\n ): Promise<{ exitCode: number }> {\n const fullConfig: LaunchConfig = {\n scenarioName: session.scenarioName,\n systemPromptPath: session.systemPromptPath,\n ...launchConfig,\n }\n\n session.aiTool = this.launcher.name\n session.startedAt = new Date().toISOString()\n await saveSession(toStoredSession(session))\n\n const proc = await this.launcher.launch(session.workdir, fullConfig)\n const result = await proc.wait()\n\n session.status = 'complete'\n session.completedAt = new Date().toISOString()\n await saveSession(toStoredSession(session))\n\n return result\n }\n\n /** Destroy a session by removing its stored data */\n async destroySession(session: Session): Promise<void> {\n await deleteSession(session.id)\n }\n\n /** Get elapsed time since the AI tool was launched, formatted as a human-readable string */\n getElapsedTime(session: Session): string | null {\n if (!session.startedAt) return null\n\n const elapsed = Date.now() - new Date(session.startedAt).getTime()\n const minutes = Math.floor(elapsed / 60000)\n const seconds = Math.floor((elapsed % 60000) / 1000)\n\n if (minutes === 0) return `${seconds}s`\n return `${minutes}m ${seconds}s`\n }\n}\n","import { readFile, writeFile, readdir, unlink, mkdir } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { homedir } from 'node:os'\nimport { existsSync } from 'node:fs'\nimport type { StoredSession } from './types.js'\n\nconst SESSIONS_DIR = join(homedir(), '.vibe-interviewing', 'sessions')\n\nasync function ensureSessionsDir(): Promise<void> {\n if (!existsSync(SESSIONS_DIR)) {\n await mkdir(SESSIONS_DIR, { recursive: true })\n }\n}\n\n/** Save a session to disk */\nexport async function saveSession(session: StoredSession): Promise<void> {\n await ensureSessionsDir()\n const filePath = join(SESSIONS_DIR, `${session.id}.json`)\n await writeFile(filePath, JSON.stringify(session, null, 2))\n}\n\n/** Load a session from disk */\nexport async function loadSession(id: string): Promise<StoredSession | null> {\n const filePath = join(SESSIONS_DIR, `${id}.json`)\n if (!existsSync(filePath)) return null\n\n const raw = await readFile(filePath, 'utf-8')\n return JSON.parse(raw) as StoredSession\n}\n\n/** Delete a session from disk */\nexport async function deleteSession(id: string): Promise<void> {\n const filePath = join(SESSIONS_DIR, `${id}.json`)\n if (existsSync(filePath)) {\n await unlink(filePath)\n }\n}\n\n/** List all stored sessions */\nexport async function listSessions(): Promise<StoredSession[]> {\n await ensureSessionsDir()\n const files = await readdir(SESSIONS_DIR)\n const sessions: StoredSession[] = []\n\n for (const file of files) {\n if (!file.endsWith('.json')) continue\n try {\n const raw = await readFile(join(SESSIONS_DIR, file), 'utf-8')\n sessions.push(JSON.parse(raw) as StoredSession)\n } catch {\n // Skip corrupted files\n }\n }\n\n return sessions.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())\n}\n\n/** List only active (non-complete) sessions */\nexport async function listActiveSessions(): Promise<StoredSession[]> {\n const all = await listSessions()\n return all.filter((s) => s.status !== 'complete')\n}\n","/** Status of an interview session */\nexport type SessionStatus = 'cloning' | 'setting-up' | 'running' | 'complete'\n\n/** A live interview session */\nexport interface Session {\n /** Unique session identifier */\n id: string\n /** Name of the scenario being run */\n scenarioName: string\n /** Local working directory for the candidate */\n workdir: string\n /** Path to the system prompt file (outside workspace) */\n systemPromptPath: string\n /** Current session status */\n status: SessionStatus\n /** ISO timestamp of session creation */\n createdAt: string\n /** ISO timestamp of when the AI tool was launched */\n startedAt?: string\n /** ISO timestamp of session completion */\n completedAt?: string\n /** Name of the AI tool used */\n aiTool?: string\n}\n\n/** Serializable session data for persistence */\nexport interface StoredSession {\n /** Unique session identifier */\n id: string\n /** Name of the scenario being run */\n scenarioName: string\n /** Current session status */\n status: SessionStatus\n /** Local working directory for the candidate */\n workdir: string\n /** Path to the system prompt file */\n systemPromptPath: string\n /** Name of the AI tool used */\n aiTool?: string\n /** ISO timestamp of session creation */\n createdAt: string\n /** ISO timestamp of when the AI tool was launched */\n startedAt?: string\n /** ISO timestamp of session completion */\n completedAt?: string\n}\n\n/** Convert a Session to a StoredSession for persistence */\nexport function toStoredSession(session: Session): StoredSession {\n return {\n id: session.id,\n scenarioName: session.scenarioName,\n status: session.status,\n workdir: session.workdir,\n systemPromptPath: session.systemPromptPath,\n aiTool: session.aiTool,\n createdAt: session.createdAt,\n startedAt: session.startedAt,\n completedAt: session.completedAt,\n }\n}\n"],"mappings":"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","names":["readFile","existsSync","parseYaml","existsSync","readFile","parseYaml","join","join","readFile","readFile","join","existsSync","readFile","readFile","writeFile","mkdir","join","homedir","readFile","writeFile","readdir","mkdir","join","homedir","existsSync","join","homedir","readFile","writeFile","mkdir"]}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@vibe-interviewing/core",
3
+ "version": "0.1.0",
4
+ "description": "Core library for vibe-interviewing — scenario engine, session management, AI tool launchers",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "dependencies": {
15
+ "simple-git": "^3.27.0",
16
+ "yaml": "^2.7.0",
17
+ "zod": "^3.24.0",
18
+ "@vibe-interviewing/scenarios": "0.1.0"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^20.17.0",
22
+ "tsup": "^8.3.0",
23
+ "typescript": "^5.7.0",
24
+ "vitest": "^3.0.0"
25
+ },
26
+ "license": "MIT",
27
+ "files": [
28
+ "dist/"
29
+ ],
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/cpaczek/vibe-interviewing.git",
33
+ "directory": "packages/core"
34
+ },
35
+ "homepage": "https://github.com/cpaczek/vibe-interviewing#readme",
36
+ "bugs": {
37
+ "url": "https://github.com/cpaczek/vibe-interviewing/issues"
38
+ },
39
+ "scripts": {
40
+ "build": "tsup",
41
+ "dev": "tsup --watch",
42
+ "test": "vitest run",
43
+ "test:watch": "vitest",
44
+ "lint": "eslint src/",
45
+ "typecheck": "tsc --noEmit"
46
+ }
47
+ }