@k-leumas/queque-cli 0.3.9 → 0.4.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/dist/cli/main.js CHANGED
@@ -28,6 +28,165 @@ import { promisify as promisify2 } from "util";
28
28
 
29
29
  // src/shared/debug-log.ts
30
30
  import * as fsp from "fs/promises";
31
+
32
+ // src/shared/qq-config.ts
33
+ import { readFileSync } from "fs";
34
+ import { homedir } from "os";
35
+ import { join } from "path";
36
+ import { z } from "zod";
37
+ var DEFAULT_SENSITIVE_PATH_PATTERNS = [
38
+ "\\.env",
39
+ "credentials",
40
+ "\\.pem$",
41
+ "id_rsa",
42
+ "\\.ssh/",
43
+ "\\.aws/",
44
+ "secret"
45
+ ];
46
+ var DEFAULT_REDACT_LOG_KEYS = ["lbuffer", "rbuffer", "queryText", "request"];
47
+ var DEFAULT_DESTRUCTIVE_COMMAND_PATTERNS = [
48
+ "\\brm\\s+(-?-?\\w+)?\\b",
49
+ "\\bsudo\\b",
50
+ "\\bchmod\\s+-R\\b",
51
+ ">\\s*/dev/",
52
+ "\\bcurl\\b[^\\n|]*\\|\\s*sh\\b",
53
+ "\\bwget\\b[^\\n|]*\\|\\s*sh\\b"
54
+ ];
55
+ var privacyConfigSchema = z.object({
56
+ sensitivePathPatterns: z.array(z.string()).optional(),
57
+ redactLogKeys: z.array(z.string()).optional(),
58
+ allowFileRead: z.boolean().optional(),
59
+ useGitignore: z.boolean().optional().describe(
60
+ "Not yet implemented \u2014 this config option will enable gitignore-based filtering in the future. Placeholder only."
61
+ )
62
+ }).optional();
63
+ var safetyConfigSchema = z.object({
64
+ destructiveCommandPatterns: z.array(z.string()).optional()
65
+ }).optional();
66
+ var qqConfigFileSchema = z.object({
67
+ privacy: privacyConfigSchema,
68
+ safety: safetyConfigSchema
69
+ });
70
+ var cachedConfig = null;
71
+ function defaultConfigPath() {
72
+ return process.env.QQ_CONFIG_FILE ?? join(homedir(), ".config", "qq", "config.json");
73
+ }
74
+ var MAX_PATTERN_LENGTH = 256;
75
+ function compilePatterns(patterns) {
76
+ const compiled = [];
77
+ for (const source of patterns) {
78
+ if (source.length > MAX_PATTERN_LENGTH) {
79
+ continue;
80
+ }
81
+ try {
82
+ compiled.push(new RegExp(source, "i"));
83
+ } catch {
84
+ }
85
+ }
86
+ return compiled;
87
+ }
88
+ function envFlagEnabled(name) {
89
+ const value = process.env[name];
90
+ return value === "1" || value === "true";
91
+ }
92
+ function readConfigFile() {
93
+ const configPath = defaultConfigPath();
94
+ let raw;
95
+ try {
96
+ raw = readFileSync(configPath, "utf8");
97
+ } catch (error) {
98
+ const code = error.code;
99
+ if (code === "ENOENT") {
100
+ return {};
101
+ }
102
+ return {};
103
+ }
104
+ try {
105
+ return qqConfigFileSchema.parse(JSON.parse(raw));
106
+ } catch (error) {
107
+ const message = error instanceof Error ? error.message : String(error);
108
+ console.warn(`queque: ignoring invalid config at ${configPath}: ${message}`);
109
+ return {};
110
+ }
111
+ }
112
+ function loadQqConfig() {
113
+ if (cachedConfig) {
114
+ return cachedConfig;
115
+ }
116
+ const file = readConfigFile();
117
+ const sensitivePatterns = [
118
+ ...DEFAULT_SENSITIVE_PATH_PATTERNS,
119
+ ...file.privacy?.sensitivePathPatterns ?? []
120
+ ];
121
+ const redactKeys = [...DEFAULT_REDACT_LOG_KEYS, ...file.privacy?.redactLogKeys ?? []];
122
+ const destructivePatterns = [
123
+ ...DEFAULT_DESTRUCTIVE_COMMAND_PATTERNS,
124
+ ...file.safety?.destructiveCommandPatterns ?? []
125
+ ];
126
+ const allowFileRead = envFlagEnabled("QQ_ALLOW_FILE_READ") || (file.privacy?.allowFileRead ?? false);
127
+ cachedConfig = {
128
+ sensitivePathRegexes: compilePatterns(sensitivePatterns),
129
+ redactLogKeys: new Set(redactKeys),
130
+ allowFileRead,
131
+ destructiveCommandRegexes: compilePatterns(destructivePatterns)
132
+ };
133
+ return cachedConfig;
134
+ }
135
+
136
+ // src/shared/privacy-filter.ts
137
+ function isSensitivePath(path2) {
138
+ const { sensitivePathRegexes } = loadQqConfig();
139
+ return sensitivePathRegexes.some((pattern) => pattern.test(path2));
140
+ }
141
+ function filterContextEnvelope(envelope) {
142
+ return {
143
+ ...envelope,
144
+ extras: envelope.extras.map((chunk) => {
145
+ if (chunk.kind !== "git") {
146
+ return chunk;
147
+ }
148
+ return {
149
+ ...chunk,
150
+ payload: {
151
+ ...chunk.payload,
152
+ changedFiles: chunk.payload.changedFiles.filter((filePath) => !isSensitivePath(filePath))
153
+ }
154
+ };
155
+ })
156
+ };
157
+ }
158
+ function redactForLog(details) {
159
+ if (process.env.QQ_DEBUG_VERBOSE === "1" || details === void 0) {
160
+ return details;
161
+ }
162
+ if (details === null || typeof details !== "object") {
163
+ return details;
164
+ }
165
+ if (Array.isArray(details)) {
166
+ return details.map((item) => redactForLog(item));
167
+ }
168
+ const { redactLogKeys } = loadQqConfig();
169
+ const input = details;
170
+ const output = {};
171
+ for (const [key, value] of Object.entries(input)) {
172
+ if (redactLogKeys.has(key)) {
173
+ if (typeof value === "string") {
174
+ output[key] = `[redacted:${value.length}chars]`;
175
+ } else {
176
+ output[key] = redactForLog(value);
177
+ }
178
+ continue;
179
+ }
180
+ output[key] = redactForLog(value);
181
+ }
182
+ return output;
183
+ }
184
+ function isDestructiveCommand(command) {
185
+ const { destructiveCommandRegexes } = loadQqConfig();
186
+ return destructiveCommandRegexes.some((pattern) => pattern.test(command));
187
+ }
188
+
189
+ // src/shared/debug-log.ts
31
190
  var debugLogPath = process.env.QQ_DEBUG_LOG_FILE ?? `/tmp/qq-${process.getuid?.() ?? "unknown"}-debug.log`;
32
191
  function formatDetails(details) {
33
192
  if (details === void 0) return "";
@@ -38,7 +197,7 @@ function formatDetails(details) {
38
197
  }
39
198
  }
40
199
  async function appendDebugLog(scope, message, details) {
41
- const line = `${(/* @__PURE__ */ new Date()).toISOString()} [${scope}] ${message}${formatDetails(details)}
200
+ const line = `${(/* @__PURE__ */ new Date()).toISOString()} [${scope}] ${message}${formatDetails(redactForLog(details))}
42
201
  `;
43
202
  try {
44
203
  await fsp.appendFile(debugLogPath, line, { encoding: "utf-8", mode: 384 });
@@ -159,6 +318,188 @@ var gitContextProvider = {
159
318
  }
160
319
  };
161
320
 
321
+ // src/providers/claude.ts
322
+ import Anthropic from "@anthropic-ai/sdk";
323
+
324
+ // src/contracts/candidates.ts
325
+ import { z as z2 } from "zod";
326
+ var commandCandidateSchema = z2.object({
327
+ command: z2.string().trim().min(1),
328
+ explanation: z2.string()
329
+ });
330
+ var candidateListSchema = z2.array(commandCandidateSchema).min(1).max(5);
331
+
332
+ // src/shared/env-file.ts
333
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
334
+ import { dirname, join as join2 } from "path";
335
+ function findUp(startDir, filename) {
336
+ let current = startDir;
337
+ while (true) {
338
+ const candidate = join2(current, filename);
339
+ if (existsSync(candidate)) {
340
+ return candidate;
341
+ }
342
+ const parent = dirname(current);
343
+ if (parent === current) {
344
+ return null;
345
+ }
346
+ current = parent;
347
+ }
348
+ }
349
+ function parseEnvFile(content) {
350
+ const values = /* @__PURE__ */ new Map();
351
+ for (const rawLine of content.split(/\r?\n/)) {
352
+ const line = rawLine.trim();
353
+ if (!line || line.startsWith("#")) {
354
+ continue;
355
+ }
356
+ const match = line.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
357
+ if (!match) {
358
+ continue;
359
+ }
360
+ const [, key, rawValue] = match;
361
+ let value = rawValue.trim();
362
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
363
+ value = value.slice(1, -1);
364
+ }
365
+ values.set(key, value);
366
+ }
367
+ return values;
368
+ }
369
+ var envCache = /* @__PURE__ */ new Map();
370
+ function readEnvValueFromDotEnvLocal(key, startDir = process.cwd()) {
371
+ const cacheKey = `${key}\0${startDir}`;
372
+ if (envCache.has(cacheKey)) {
373
+ return envCache.get(cacheKey) ?? null;
374
+ }
375
+ const envPath = findUp(startDir, ".env.local");
376
+ if (!envPath) {
377
+ envCache.set(cacheKey, null);
378
+ return null;
379
+ }
380
+ const values = parseEnvFile(readFileSync2(envPath, "utf8"));
381
+ const result = values.get(key) ?? null;
382
+ envCache.set(cacheKey, result);
383
+ return result;
384
+ }
385
+
386
+ // src/providers/claude.ts
387
+ var DEFAULT_MODEL = "claude-haiku-4-5-20251001";
388
+ function resolveModel() {
389
+ return process.env.QQ_MODEL ?? readEnvValueFromDotEnvLocal("QQ_MODEL") ?? DEFAULT_MODEL;
390
+ }
391
+ function extractText(content) {
392
+ return content.filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text ?? "").join("").trim();
393
+ }
394
+ function stripCodeFence(raw) {
395
+ const m = raw.trim().match(/^```(?:json)?\s*\n?([\s\S]*?)\n?```\s*$/);
396
+ return m ? m[1].trim() : raw.trim();
397
+ }
398
+ function parseCandidates(text) {
399
+ let parsed;
400
+ try {
401
+ parsed = JSON.parse(stripCodeFence(text));
402
+ } catch {
403
+ const trimmed = text.trim();
404
+ return [{ command: trimmed || 'echo ""', explanation: "" }];
405
+ }
406
+ try {
407
+ return candidateListSchema.parse(parsed);
408
+ } catch {
409
+ return [{ command: 'echo ""', explanation: "QueQue: unexpected response format" }];
410
+ }
411
+ }
412
+ function shouldForceSelector() {
413
+ const configured = process.env.QQ_FORCE_SELECTOR ?? readEnvValueFromDotEnvLocal("QQ_FORCE_SELECTOR");
414
+ return configured === "1" || configured === "true";
415
+ }
416
+ function ensureSelectableCandidates(candidates) {
417
+ if (!shouldForceSelector() || candidates.length >= 2) {
418
+ return candidates;
419
+ }
420
+ const [first] = candidates;
421
+ return [
422
+ first,
423
+ {
424
+ command: first.command,
425
+ explanation: first.explanation.length > 0 ? `${first.explanation} (forced duplicate for selector testing)` : "Forced duplicate for selector testing."
426
+ }
427
+ ];
428
+ }
429
+ function buildPrompt(envelope) {
430
+ const filtered = filterContextEnvelope(envelope);
431
+ const gitChunk = filtered.extras.find((chunk) => chunk.kind === "git");
432
+ const filesystemChunk = filtered.extras.find((chunk) => chunk.kind === "filesystem");
433
+ return [
434
+ "Return ONLY a JSON array of 1-3 shell command candidates, most likely first.",
435
+ 'Each item must have exactly these keys: "command" and "explanation".',
436
+ "No markdown, no prose, no code fences. Raw JSON array only.",
437
+ "",
438
+ "The command should be safe to place back into a shell buffer.",
439
+ "",
440
+ "Shell context:",
441
+ JSON.stringify(
442
+ {
443
+ cwd: filtered.base.cwd,
444
+ queryText: filtered.base.queryText,
445
+ platform: filtered.base.platform,
446
+ shellName: filtered.base.shellName,
447
+ ...gitChunk ? { versionControl: gitChunk.payload } : {},
448
+ ...filesystemChunk ? { filesystem: filesystemChunk.payload } : {}
449
+ },
450
+ null,
451
+ 2
452
+ )
453
+ ].join("\n");
454
+ }
455
+ async function fetchCandidates(envelope, rbuffer = "") {
456
+ const apiKey = process.env.ANTHROPIC_API_KEY ?? readEnvValueFromDotEnvLocal("ANTHROPIC_API_KEY");
457
+ if (!apiKey) {
458
+ throw new Error("ANTHROPIC_API_KEY is required in the environment or .env.local");
459
+ }
460
+ const client = new Anthropic({ apiKey });
461
+ const prompt = buildPrompt(envelope);
462
+ const model = resolveModel();
463
+ void appendDebugLog("provider", "request start", {
464
+ model,
465
+ cwd: envelope.base.cwd,
466
+ rbufferLength: rbuffer.length,
467
+ extras: envelope.extras.map((chunk) => chunk.kind)
468
+ });
469
+ try {
470
+ const response = await client.messages.create(
471
+ {
472
+ model,
473
+ max_tokens: 1024,
474
+ temperature: 0,
475
+ system: "You are QueQue, a terminal shell assistant. Return ONLY a JSON array of command candidates, ranked with the most correct/direct command first. No prose, no markdown, no code fences. When a command requires a value the user must supply (hostname, filename, branch name, etc.), wrap it in angle brackets: <placeholder>. Use descriptive names like <user@host>, <filename>, <branch-name>. Do not use angle brackets for optional flags or known values.",
476
+ messages: [{ role: "user", content: prompt }]
477
+ },
478
+ {
479
+ timeout: 25e3
480
+ // 25s — slightly under the zsh 30s FIFO timeout
481
+ }
482
+ );
483
+ const text = extractText(response.content);
484
+ const candidates = ensureSelectableCandidates(parseCandidates(text));
485
+ void appendDebugLog("provider", "response parsed", {
486
+ model,
487
+ candidateCount: candidates.length,
488
+ forceSelector: shouldForceSelector()
489
+ });
490
+ return candidates;
491
+ } catch (error) {
492
+ void appendDebugLog("provider", "request failed", {
493
+ model,
494
+ message: error instanceof Error ? error.message : String(error)
495
+ });
496
+ throw error;
497
+ }
498
+ }
499
+ var claudeAdapter = {
500
+ fetchCandidates
501
+ };
502
+
162
503
  // src/registry/context-providers.ts
163
504
  var registry = /* @__PURE__ */ new Map();
164
505
  function registerContextProvider(descriptor) {
@@ -173,11 +514,14 @@ function listContextProviders() {
173
514
 
174
515
  // src/registry/provider-backends.ts
175
516
  var registry2 = /* @__PURE__ */ new Map();
176
- function registerProviderBackend(descriptor) {
177
- if (registry2.has(descriptor.id)) {
178
- throw new Error(`Provider backend already registered: "${descriptor.id}"`);
517
+ function registerProviderBackend(registration) {
518
+ if (registry2.has(registration.id)) {
519
+ throw new Error(`Provider backend already registered: "${registration.id}"`);
179
520
  }
180
- registry2.set(descriptor.id, descriptor);
521
+ registry2.set(registration.id, registration);
522
+ }
523
+ function getProviderAdapter(id) {
524
+ return registry2.get(id)?.adapter;
181
525
  }
182
526
 
183
527
  // src/registry/shell-adapters.ts
@@ -188,6 +532,9 @@ function registerShellAdapter(descriptor) {
188
532
  }
189
533
  registry3.set(descriptor.id, descriptor);
190
534
  }
535
+ function listShellAdapters() {
536
+ return Array.from(registry3.values());
537
+ }
191
538
 
192
539
  // src/registry/storage-hooks.ts
193
540
  var registry4 = /* @__PURE__ */ new Map();
@@ -223,7 +570,8 @@ function bootstrapBuiltins() {
223
570
  registerProviderBackend({
224
571
  id: "claude",
225
572
  name: "Claude (Anthropic)",
226
- description: "Anthropic Claude adapter \u2014 default LLM backend"
573
+ description: "Anthropic Claude adapter \u2014 default LLM backend",
574
+ adapter: claudeAdapter
227
575
  });
228
576
  }
229
577
 
@@ -234,36 +582,36 @@ import { render } from "ink";
234
582
  import React from "react";
235
583
 
236
584
  // src/contracts/request.ts
237
- import { z as z2 } from "zod";
585
+ import { z as z4 } from "zod";
238
586
 
239
587
  // src/contracts/shell.ts
240
- import { z } from "zod";
241
- var shellRequestSchema = z.object({
242
- version: z.literal(1),
243
- ttyPath: z.string(),
244
- cwd: z.string().regex(/^\/[^\0]*$/, "cwd must be an absolute POSIX path"),
245
- shellPid: z.number().int().positive(),
246
- lbuffer: z.string(),
247
- rbuffer: z.string()
588
+ import { z as z3 } from "zod";
589
+ var shellRequestSchema = z3.object({
590
+ version: z3.literal(1),
591
+ ttyPath: z3.string(),
592
+ cwd: z3.string().regex(/^\/[^\0]*$/, "cwd must be an absolute POSIX path"),
593
+ shellPid: z3.number().int().positive(),
594
+ lbuffer: z3.string(),
595
+ rbuffer: z3.string()
248
596
  });
249
- var shellResultSchema = z.discriminatedUnion("kind", [
250
- z.object({
251
- kind: z.literal("cancel")
597
+ var shellResultSchema = z3.discriminatedUnion("kind", [
598
+ z3.object({
599
+ kind: z3.literal("cancel")
252
600
  }).strict(),
253
- z.object({
254
- kind: z.literal("replace-buffer"),
255
- lbuffer: z.string(),
256
- rbuffer: z.string(),
257
- query: z.string().optional()
601
+ z3.object({
602
+ kind: z3.literal("replace-buffer"),
603
+ lbuffer: z3.string(),
604
+ rbuffer: z3.string(),
605
+ query: z3.string().optional()
258
606
  }),
259
- z.object({
260
- kind: z.literal("error"),
261
- message: z.string()
607
+ z3.object({
608
+ kind: z3.literal("error"),
609
+ message: z3.string()
262
610
  })
263
611
  ]);
264
612
 
265
613
  // src/contracts/request.ts
266
- var requestIntentSchema = z2.enum([
614
+ var requestIntentSchema = z4.enum([
267
615
  "shell-command",
268
616
  "codebase",
269
617
  "filesystem",
@@ -272,47 +620,47 @@ var requestIntentSchema = z2.enum([
272
620
  ]);
273
621
  var normalizedRequestSchema = shellRequestSchema.extend({
274
622
  intent: requestIntentSchema,
275
- confidence: z2.number().min(0).max(1)
623
+ confidence: z4.number().min(0).max(1)
276
624
  });
277
- var baseContextSchema = z2.object({
278
- queryText: z2.string(),
279
- cwd: z2.string(),
280
- ttyPath: z2.string(),
281
- shellPid: z2.number().int().positive(),
625
+ var baseContextSchema = z4.object({
626
+ queryText: z4.string(),
627
+ cwd: z4.string(),
628
+ ttyPath: z4.string(),
629
+ shellPid: z4.number().int().positive(),
282
630
  /** Hardcoded 'zsh' for Phase 2 — cross-shell support is v2 scope. */
283
- shellName: z2.literal("zsh"),
284
- platform: z2.string(),
285
- timestamp: z2.string()
631
+ shellName: z4.literal("zsh"),
632
+ platform: z4.string(),
633
+ timestamp: z4.string()
286
634
  });
287
- var intentDecisionSchema = z2.object({
635
+ var intentDecisionSchema = z4.object({
288
636
  intent: requestIntentSchema,
289
- confidence: z2.number().min(0).max(1),
290
- signals: z2.array(z2.string())
637
+ confidence: z4.number().min(0).max(1),
638
+ signals: z4.array(z4.string())
291
639
  });
292
- var contextChunkSchema = z2.discriminatedUnion("kind", [
293
- z2.object({
294
- kind: z2.literal("git"),
295
- payload: z2.object({
296
- cwd: z2.string(),
297
- root: z2.string(),
298
- branch: z2.string().nullable(),
299
- dirty: z2.boolean(),
640
+ var contextChunkSchema = z4.discriminatedUnion("kind", [
641
+ z4.object({
642
+ kind: z4.literal("git"),
643
+ payload: z4.object({
644
+ cwd: z4.string(),
645
+ root: z4.string(),
646
+ branch: z4.string().nullable(),
647
+ dirty: z4.boolean(),
300
648
  /** File paths only (from git status --porcelain). No diff content, no file bytes. */
301
- changedFiles: z2.array(z2.string())
649
+ changedFiles: z4.array(z4.string())
302
650
  })
303
651
  }),
304
- z2.object({
305
- kind: z2.literal("filesystem"),
306
- payload: z2.object({
307
- cwd: z2.string(),
652
+ z4.object({
653
+ kind: z4.literal("filesystem"),
654
+ payload: z4.object({
655
+ cwd: z4.string(),
308
656
  /** Apparent filename extracted from query text. No file bytes read. */
309
- apparentFilename: z2.string().nullable()
657
+ apparentFilename: z4.string().nullable()
310
658
  })
311
659
  })
312
660
  ]);
313
- var contextEnvelopeSchema = z2.object({
661
+ var contextEnvelopeSchema = z4.object({
314
662
  base: baseContextSchema,
315
- extras: z2.array(contextChunkSchema)
663
+ extras: z4.array(contextChunkSchema)
316
664
  });
317
665
 
318
666
  // src/intent/router.ts
@@ -388,7 +736,7 @@ function buildBaseContext(request) {
388
736
  // src/context/pipeline.ts
389
737
  async function gatherContext(request) {
390
738
  void appendDebugLog("context", "pipeline start", {
391
- lbuffer: request.lbuffer,
739
+ queryLength: request.lbuffer.length,
392
740
  intent: request.intent
393
741
  });
394
742
  const base = buildBaseContext(request);
@@ -421,7 +769,7 @@ async function gatherContext(request) {
421
769
  });
422
770
  }
423
771
  }
424
- const envelope = { base, extras };
772
+ const envelope = filterContextEnvelope({ base, extras });
425
773
  void appendDebugLog("context", "envelope assembled", { extraCount: extras.length });
426
774
  return envelope;
427
775
  }
@@ -499,182 +847,6 @@ async function ensureDaemon(socketPath2) {
499
847
  void appendDebugLog("daemon", "daemon ready", { socketPath: socketPath2 });
500
848
  }
501
849
 
502
- // src/providers/claude.ts
503
- import Anthropic from "@anthropic-ai/sdk";
504
-
505
- // src/contracts/candidates.ts
506
- import { z as z3 } from "zod";
507
- var commandCandidateSchema = z3.object({
508
- command: z3.string().min(1),
509
- explanation: z3.string()
510
- });
511
- var candidateListSchema = z3.array(commandCandidateSchema).min(1).max(5);
512
-
513
- // src/shared/env-file.ts
514
- import { existsSync as existsSync2, readFileSync } from "fs";
515
- import { dirname as dirname2, join } from "path";
516
- function findUp(startDir, filename) {
517
- let current = startDir;
518
- while (true) {
519
- const candidate = join(current, filename);
520
- if (existsSync2(candidate)) {
521
- return candidate;
522
- }
523
- const parent = dirname2(current);
524
- if (parent === current) {
525
- return null;
526
- }
527
- current = parent;
528
- }
529
- }
530
- function parseEnvFile(content) {
531
- const values = /* @__PURE__ */ new Map();
532
- for (const rawLine of content.split(/\r?\n/)) {
533
- const line = rawLine.trim();
534
- if (!line || line.startsWith("#")) {
535
- continue;
536
- }
537
- const match = line.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
538
- if (!match) {
539
- continue;
540
- }
541
- const [, key, rawValue] = match;
542
- let value = rawValue.trim();
543
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
544
- value = value.slice(1, -1);
545
- }
546
- values.set(key, value);
547
- }
548
- return values;
549
- }
550
- var envCache = /* @__PURE__ */ new Map();
551
- function readEnvValueFromDotEnvLocal(key, startDir = process.cwd()) {
552
- const cacheKey = `${key}\0${startDir}`;
553
- if (envCache.has(cacheKey)) {
554
- return envCache.get(cacheKey) ?? null;
555
- }
556
- const envPath = findUp(startDir, ".env.local");
557
- if (!envPath) {
558
- envCache.set(cacheKey, null);
559
- return null;
560
- }
561
- const values = parseEnvFile(readFileSync(envPath, "utf8"));
562
- const result = values.get(key) ?? null;
563
- envCache.set(cacheKey, result);
564
- return result;
565
- }
566
-
567
- // src/providers/claude.ts
568
- var DEFAULT_MODEL = "claude-haiku-4-5-20251001";
569
- function resolveModel() {
570
- return process.env.QQ_MODEL ?? readEnvValueFromDotEnvLocal("QQ_MODEL") ?? DEFAULT_MODEL;
571
- }
572
- function extractText(content) {
573
- return content.filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text ?? "").join("").trim();
574
- }
575
- function stripCodeFence(raw) {
576
- const m = raw.trim().match(/^```(?:json)?\s*\n?([\s\S]*?)\n?```\s*$/);
577
- return m ? m[1].trim() : raw.trim();
578
- }
579
- function parseCandidates(text) {
580
- let parsed;
581
- try {
582
- parsed = JSON.parse(stripCodeFence(text));
583
- } catch {
584
- const trimmed = text.trim();
585
- return [{ command: trimmed || 'echo ""', explanation: "" }];
586
- }
587
- try {
588
- return candidateListSchema.parse(parsed);
589
- } catch {
590
- return [{ command: 'echo ""', explanation: "QueQue: unexpected response format" }];
591
- }
592
- }
593
- function shouldForceSelector() {
594
- const configured = process.env.QQ_FORCE_SELECTOR ?? readEnvValueFromDotEnvLocal("QQ_FORCE_SELECTOR");
595
- return configured === "1" || configured === "true";
596
- }
597
- function ensureSelectableCandidates(candidates) {
598
- if (!shouldForceSelector() || candidates.length >= 2) {
599
- return candidates;
600
- }
601
- const [first] = candidates;
602
- return [
603
- first,
604
- {
605
- command: first.command,
606
- explanation: first.explanation.length > 0 ? `${first.explanation} (forced duplicate for selector testing)` : "Forced duplicate for selector testing."
607
- }
608
- ];
609
- }
610
- function buildPrompt(envelope) {
611
- const gitChunk = envelope.extras.find((chunk) => chunk.kind === "git");
612
- return [
613
- "Return ONLY a JSON array of 1-3 shell command candidates, most likely first.",
614
- 'Each item must have exactly these keys: "command" and "explanation".',
615
- "No markdown, no prose, no code fences. Raw JSON array only.",
616
- "",
617
- "The command should be safe to place back into a shell buffer.",
618
- "",
619
- "Shell context:",
620
- JSON.stringify(
621
- {
622
- cwd: envelope.base.cwd,
623
- queryText: envelope.base.queryText,
624
- platform: envelope.base.platform,
625
- shellName: envelope.base.shellName,
626
- ...gitChunk ? { versionControl: gitChunk.payload } : {}
627
- },
628
- null,
629
- 2
630
- )
631
- ].join("\n");
632
- }
633
- async function fetchCandidates(envelope, rbuffer = "") {
634
- const apiKey = process.env.ANTHROPIC_API_KEY ?? readEnvValueFromDotEnvLocal("ANTHROPIC_API_KEY");
635
- if (!apiKey) {
636
- throw new Error("ANTHROPIC_API_KEY is required in the environment or .env.local");
637
- }
638
- const client = new Anthropic({ apiKey });
639
- const prompt = buildPrompt(envelope);
640
- const model = resolveModel();
641
- void appendDebugLog("provider", "request start", {
642
- model,
643
- cwd: envelope.base.cwd,
644
- rbufferLength: rbuffer.length,
645
- extras: envelope.extras.map((chunk) => chunk.kind)
646
- });
647
- try {
648
- const response = await client.messages.create(
649
- {
650
- model,
651
- max_tokens: 1024,
652
- temperature: 0,
653
- system: "You are QueQue, a terminal shell assistant. Return ONLY a JSON array of command candidates, ranked with the most correct/direct command first. No prose, no markdown, no code fences. When a command requires a value the user must supply (hostname, filename, branch name, etc.), wrap it in angle brackets: <placeholder>. Use descriptive names like <user@host>, <filename>, <branch-name>. Do not use angle brackets for optional flags or known values.",
654
- messages: [{ role: "user", content: prompt }]
655
- },
656
- {
657
- timeout: 25e3
658
- // 25s — slightly under the zsh 30s FIFO timeout
659
- }
660
- );
661
- const text = extractText(response.content);
662
- const candidates = ensureSelectableCandidates(parseCandidates(text));
663
- void appendDebugLog("provider", "response parsed", {
664
- model,
665
- candidateCount: candidates.length,
666
- forceSelector: shouldForceSelector()
667
- });
668
- return candidates;
669
- } catch (error) {
670
- void appendDebugLog("provider", "request failed", {
671
- model,
672
- message: error instanceof Error ? error.message : String(error)
673
- });
674
- throw error;
675
- }
676
- }
677
-
678
850
  // src/providers/detect.ts
679
851
  import { execSync } from "child_process";
680
852
  import * as fsp2 from "fs/promises";
@@ -728,6 +900,35 @@ async function detectProvider() {
728
900
  };
729
901
  }
730
902
 
903
+ // src/providers/resolver.ts
904
+ function resolveAdapter(detected) {
905
+ switch (detected.kind) {
906
+ case "anthropic-key": {
907
+ const adapter = getProviderAdapter("claude");
908
+ if (!adapter) {
909
+ throw new Error(
910
+ "QueQue: Claude provider is not registered \u2014 was bootstrapBuiltins() called?"
911
+ );
912
+ }
913
+ return adapter;
914
+ }
915
+ case "claude-cli":
916
+ throw new Error(
917
+ "QueQue: Claude CLI detected but subprocess adapter is not wired yet (Phase 8). Set ANTHROPIC_API_KEY or use .env.local."
918
+ );
919
+ case "ollama":
920
+ throw new Error(
921
+ "QueQue: Ollama detected but local adapter is not wired yet (Phase 8). Set ANTHROPIC_API_KEY or use .env.local."
922
+ );
923
+ case "openai-key":
924
+ throw new Error(
925
+ "QueQue: OPENAI_API_KEY detected but OpenAI adapter is not wired yet (Phase 8). Set ANTHROPIC_API_KEY for Claude."
926
+ );
927
+ case "none":
928
+ throw new Error(detected.message);
929
+ }
930
+ }
931
+
731
932
  // src/shared/socket-path.ts
732
933
  function socketPathForUid(uid) {
733
934
  return `/tmp/qq-${uid}.sock`;
@@ -737,7 +938,7 @@ function socketPath() {
737
938
  }
738
939
 
739
940
  // src/ui/CandidateSelect.tsx
740
- import { Box as Box4, Text as Text4, useInput, useStdin } from "ink";
941
+ import { Box as Box4, Text as Text4, useInput, useStdin, useStdout as useStdout2 } from "ink";
741
942
  import { useEffect as useEffect2, useState as useState2 } from "react";
742
943
 
743
944
  // src/ui/ControlsLine.tsx
@@ -781,6 +982,88 @@ function Modal({ children }) {
781
982
  return /* @__PURE__ */ jsx3(Box2, { flexDirection: "column", paddingX: 1, width, children });
782
983
  }
783
984
 
985
+ // src/ui/modal-layout.ts
986
+ function wrapText(text, maxWidth) {
987
+ if (maxWidth < 1) {
988
+ return [text];
989
+ }
990
+ const words = text.split(/\s+/).filter((word) => word.length > 0);
991
+ if (words.length === 0) {
992
+ return [""];
993
+ }
994
+ const lines = [];
995
+ let line = "";
996
+ for (const word of words) {
997
+ let remaining = word;
998
+ while (remaining.length > 0) {
999
+ const candidate = line ? `${line} ${remaining}` : remaining;
1000
+ if (candidate.length <= maxWidth) {
1001
+ line = candidate;
1002
+ remaining = "";
1003
+ break;
1004
+ }
1005
+ if (line) {
1006
+ lines.push(line);
1007
+ line = "";
1008
+ continue;
1009
+ }
1010
+ lines.push(remaining.slice(0, maxWidth));
1011
+ remaining = remaining.slice(maxWidth);
1012
+ }
1013
+ }
1014
+ if (line) {
1015
+ lines.push(line);
1016
+ }
1017
+ return lines;
1018
+ }
1019
+ function normalizeCommand(command) {
1020
+ return command.trim();
1021
+ }
1022
+ function filterCandidates(candidates, query) {
1023
+ if (!query) return candidates;
1024
+ const lower = query.toLowerCase();
1025
+ return candidates.filter((c) => normalizeCommand(c.command).toLowerCase().includes(lower));
1026
+ }
1027
+ function estimateCandidateSelectLines(input) {
1028
+ let lines = 0;
1029
+ if (input.initialQuery && input.initialQuery.length > 0) {
1030
+ lines += 1;
1031
+ }
1032
+ lines += 1;
1033
+ lines += 1;
1034
+ if (input.error === true) {
1035
+ lines += 1;
1036
+ } else if (input.candidates === null) {
1037
+ lines += 1;
1038
+ } else {
1039
+ const visible2 = filterCandidates(input.candidates, input.query);
1040
+ if (visible2.length === 0) {
1041
+ lines += 1;
1042
+ } else {
1043
+ for (const candidate of visible2) {
1044
+ const command = normalizeCommand(candidate.command);
1045
+ const commandLines = wrapText(command, input.contentWidth);
1046
+ const explanationLines = candidate.explanation.length > 0 ? wrapText(candidate.explanation, input.contentWidth) : [];
1047
+ lines += commandLines.length + explanationLines.length;
1048
+ lines += 1;
1049
+ }
1050
+ }
1051
+ }
1052
+ const visible = input.candidates !== null && input.error !== true ? filterCandidates(input.candidates, input.query) : [];
1053
+ const selectedCommand = visible[input.selectedIndex]?.command ? normalizeCommand(visible[input.selectedIndex].command) : void 0;
1054
+ const showDestructiveWarning = selectedCommand !== void 0 && isDestructiveCommand(selectedCommand);
1055
+ if (showDestructiveWarning) {
1056
+ lines += 1;
1057
+ lines += 1;
1058
+ }
1059
+ lines += 1;
1060
+ lines += 1;
1061
+ if (input.includeDevFooter) {
1062
+ lines += 1;
1063
+ }
1064
+ return lines;
1065
+ }
1066
+
784
1067
  // src/ui/SearchInput.tsx
785
1068
  import { Box as Box3, Text as Text3 } from "ink";
786
1069
  import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
@@ -793,19 +1076,34 @@ function SearchInput({ query }) {
793
1076
 
794
1077
  // src/ui/CandidateSelect.tsx
795
1078
  import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
796
- function filterCandidates(candidates, query) {
797
- if (!query) return candidates;
798
- const lower = query.toLowerCase();
799
- return candidates.filter((c) => c.command.toLowerCase().includes(lower));
1079
+ var ROW_GUTTER = " ";
1080
+ var ACTIVE_HEAD = "\u250C> ";
1081
+ var ACTIVE_TAIL = "\u2502 ";
1082
+ var DESTRUCTIVE_WARNING_MARK = "\u26A0";
1083
+ var DESTRUCTIVE_WARNING_TEXT = `${DESTRUCTIVE_WARNING_MARK} review carefully \u2014 this command may be destructive`;
1084
+ var MODAL_MAX_WIDTH = 80;
1085
+ var MODAL_PADDING_X = 2;
1086
+ function linePrefix(active, lineIndex) {
1087
+ if (!active) {
1088
+ return ROW_GUTTER;
1089
+ }
1090
+ return lineIndex === 0 ? ACTIVE_HEAD : ACTIVE_TAIL;
1091
+ }
1092
+ function normalizeCommand2(command) {
1093
+ return command.trim();
800
1094
  }
801
1095
  function CandidateSelect({
802
1096
  candidates,
803
1097
  initialQuery,
804
1098
  onSelect,
805
1099
  onCancel,
806
- error
1100
+ error,
1101
+ onLayoutLinesChange
807
1102
  }) {
808
1103
  const { isRawModeSupported } = useStdin();
1104
+ const { stdout } = useStdout2();
1105
+ const modalWidth = Math.min(MODAL_MAX_WIDTH, stdout.columns || MODAL_MAX_WIDTH);
1106
+ const contentWidth = modalWidth - MODAL_PADDING_X - ROW_GUTTER.length;
809
1107
  const [selectedIndex, setSelectedIndex] = useState2(0);
810
1108
  const [query, setQuery] = useState2(initialQuery ?? "");
811
1109
  useEffect2(() => {
@@ -822,6 +1120,35 @@ function CandidateSelect({
822
1120
  useEffect2(() => {
823
1121
  setSelectedIndex(0);
824
1122
  }, [query]);
1123
+ const includeDevFooter = process.env.QQ_DEV_ROOT != null;
1124
+ useEffect2(() => {
1125
+ if (!onLayoutLinesChange) {
1126
+ return;
1127
+ }
1128
+ if (candidates === null && error !== true) {
1129
+ return;
1130
+ }
1131
+ onLayoutLinesChange(
1132
+ estimateCandidateSelectLines({
1133
+ candidates,
1134
+ initialQuery,
1135
+ query,
1136
+ selectedIndex,
1137
+ error,
1138
+ contentWidth,
1139
+ includeDevFooter
1140
+ })
1141
+ );
1142
+ }, [
1143
+ candidates,
1144
+ initialQuery,
1145
+ query,
1146
+ selectedIndex,
1147
+ error,
1148
+ contentWidth,
1149
+ includeDevFooter,
1150
+ onLayoutLinesChange
1151
+ ]);
825
1152
  useInput(
826
1153
  (input, key) => {
827
1154
  if (key.escape) {
@@ -836,8 +1163,9 @@ function CandidateSelect({
836
1163
  const visible = filterCandidates(candidates, query);
837
1164
  if (visible.length === 0) return;
838
1165
  const selected = visible[selectedIndex] ?? visible[0];
839
- const explanation = selected.explanation || `see man ${selected.command.split(" ")[0]}`;
840
- onSelect(selected.command, explanation);
1166
+ const command = normalizeCommand2(selected.command);
1167
+ const explanation = selected.explanation || `see man ${command.split(" ")[0]}`;
1168
+ onSelect(command, explanation);
841
1169
  return;
842
1170
  }
843
1171
  if (input && !key.ctrl && !key.meta) {
@@ -858,27 +1186,51 @@ function CandidateSelect({
858
1186
  { isActive: isRawModeSupported }
859
1187
  );
860
1188
  let content;
1189
+ let showDestructiveWarning = false;
861
1190
  if (error === true) {
862
1191
  content = /* @__PURE__ */ jsx5(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text4, { dimColor: true, children: "provider error \u2014 press esc to return" }) });
863
1192
  } else if (candidates === null) {
864
1193
  content = /* @__PURE__ */ jsx5(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx5(LoadingSpinner, {}) });
865
1194
  } else {
866
1195
  const visible = filterCandidates(candidates, query);
1196
+ const selectedCommand = visible[selectedIndex]?.command ? normalizeCommand2(visible[selectedIndex].command) : void 0;
1197
+ showDestructiveWarning = selectedCommand !== void 0 && isDestructiveCommand(selectedCommand);
867
1198
  if (visible.length === 0) {
868
1199
  content = /* @__PURE__ */ jsx5(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text4, { dimColor: true, children: "no matches \u2014 try a different query" }) });
869
1200
  } else {
870
1201
  content = /* @__PURE__ */ jsx5(Box4, { flexDirection: "column", marginTop: 1, children: visible.map((candidate, index) => {
871
1202
  const active = index === selectedIndex;
872
- return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", marginBottom: 1, children: [
873
- /* @__PURE__ */ jsxs3(Box4, { children: [
874
- active ? /* @__PURE__ */ jsx5(Text4, { color: "ansi256(166)", children: "\u250C> " }) : /* @__PURE__ */ jsx5(Text4, { children: " " }),
875
- /* @__PURE__ */ jsx5(Text4, { bold: active, color: active ? "white" : void 0, dimColor: !active, children: candidate.command })
876
- ] }),
877
- candidate.explanation.length > 0 && /* @__PURE__ */ jsxs3(Box4, { children: [
878
- /* @__PURE__ */ jsx5(Text4, { children: " " }),
879
- /* @__PURE__ */ jsx5(Text4, { dimColor: true, children: candidate.explanation })
880
- ] })
881
- ] }, candidate.command);
1203
+ const command = normalizeCommand2(candidate.command);
1204
+ const destructive = isDestructiveCommand(command);
1205
+ const commandLines = wrapText(command, contentWidth);
1206
+ const explanationLines = candidate.explanation.length > 0 ? wrapText(candidate.explanation, contentWidth) : [];
1207
+ const blockLines = [...commandLines, ...explanationLines];
1208
+ return /* @__PURE__ */ jsx5(
1209
+ Box4,
1210
+ {
1211
+ flexDirection: "column",
1212
+ marginBottom: 1,
1213
+ children: blockLines.map((line, lineIndex) => {
1214
+ const isCommandLine = lineIndex < commandLines.length;
1215
+ const isLastCommandLine = isCommandLine && lineIndex === commandLines.length - 1;
1216
+ const prefix = linePrefix(active, lineIndex);
1217
+ return /* @__PURE__ */ jsxs3(Box4, { children: [
1218
+ /* @__PURE__ */ jsx5(Text4, { color: active ? "ansi256(166)" : void 0, children: prefix }),
1219
+ /* @__PURE__ */ jsx5(
1220
+ Text4,
1221
+ {
1222
+ bold: active && isCommandLine,
1223
+ color: active && isCommandLine ? "white" : void 0,
1224
+ dimColor: !active || !isCommandLine,
1225
+ children: line
1226
+ }
1227
+ ),
1228
+ isLastCommandLine && destructive && /* @__PURE__ */ jsx5(Text4, { color: "yellow", children: ` ${DESTRUCTIVE_WARNING_MARK}` })
1229
+ ] }, `${prefix}${line}`);
1230
+ })
1231
+ },
1232
+ `${command}::${candidate.explanation}`
1233
+ );
882
1234
  }) });
883
1235
  }
884
1236
  }
@@ -886,8 +1238,9 @@ function CandidateSelect({
886
1238
  initialQuery && initialQuery.length > 0 && /* @__PURE__ */ jsx5(Box4, { flexGrow: 0, height: 1, children: /* @__PURE__ */ jsx5(Text4, { dimColor: true, wrap: "truncate", children: `queque \u203A ${initialQuery}` }) }),
887
1239
  /* @__PURE__ */ jsx5(SearchInput, { query }),
888
1240
  content,
1241
+ showDestructiveWarning && /* @__PURE__ */ jsx5(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text4, { color: "yellow", children: DESTRUCTIVE_WARNING_TEXT }) }),
889
1242
  /* @__PURE__ */ jsx5(ControlsLine, {}),
890
- process.env.QQ_DEV_ROOT != null && /* @__PURE__ */ jsx5(Box4, { justifyContent: "flex-end", children: /* @__PURE__ */ jsx5(Text4, { color: "yellow", children: `dev v${"0.3.9"} ${"c770054"}` }) })
1243
+ process.env.QQ_DEV_ROOT != null && /* @__PURE__ */ jsx5(Box4, { justifyContent: "flex-end", children: /* @__PURE__ */ jsx5(Text4, { color: "yellow", children: `dev v${"0.4.0"} ${"1f82d23"}` }) })
891
1244
  ] });
892
1245
  }
893
1246
 
@@ -912,6 +1265,51 @@ async function writeShellResult(resultFile, result) {
912
1265
  }
913
1266
  }
914
1267
 
1268
+ // src/client/zellij-pane-resize.ts
1269
+ import { spawn as spawn2 } from "child_process";
1270
+ var DEFAULT_MAX_PANE_HEIGHT = 24;
1271
+ var MIN_PANE_HEIGHT = 6;
1272
+ var ZELLIJ_FLOATING_FRAME_LINES = 2;
1273
+ var lastSyncedHeight;
1274
+ function readMaxZellijPaneHeight() {
1275
+ const parsed = Number.parseInt(process.env.QQ_PANE_HEIGHT ?? "", 10);
1276
+ if (Number.isFinite(parsed) && parsed >= MIN_PANE_HEIGHT) {
1277
+ return parsed;
1278
+ }
1279
+ return DEFAULT_MAX_PANE_HEIGHT;
1280
+ }
1281
+ function resolveZellijPaneHeight(contentLines, maxHeight = readMaxZellijPaneHeight()) {
1282
+ const withFrame = contentLines + ZELLIJ_FLOATING_FRAME_LINES;
1283
+ return Math.min(Math.max(withFrame, MIN_PANE_HEIGHT), maxHeight);
1284
+ }
1285
+ function formatZellijPaneId(rawPaneId) {
1286
+ if (rawPaneId.startsWith("terminal_") || rawPaneId.startsWith("plugin_")) {
1287
+ return rawPaneId;
1288
+ }
1289
+ return `terminal_${rawPaneId}`;
1290
+ }
1291
+ function syncZellijFloatingPaneHeight(contentLines) {
1292
+ if (process.env.ZELLIJ === void 0) {
1293
+ return;
1294
+ }
1295
+ const rawPaneId = process.env.ZELLIJ_PANE_ID;
1296
+ if (rawPaneId === void 0 || rawPaneId.length === 0) {
1297
+ return;
1298
+ }
1299
+ const height = resolveZellijPaneHeight(contentLines);
1300
+ if (height === lastSyncedHeight) {
1301
+ return;
1302
+ }
1303
+ lastSyncedHeight = height;
1304
+ const paneId = formatZellijPaneId(rawPaneId);
1305
+ const child = spawn2(
1306
+ "zellij",
1307
+ ["action", "change-floating-pane-coordinates", "--pane-id", paneId, "--height", String(height)],
1308
+ { stdio: "ignore" }
1309
+ );
1310
+ child.unref();
1311
+ }
1312
+
915
1313
  // src/client/run-foreground.ts
916
1314
  function buildShellBuffers(command, explanation) {
917
1315
  const comment = ` # ${explanation}`;
@@ -941,7 +1339,12 @@ async function runForegroundClient(args) {
941
1339
  try {
942
1340
  const raw = await fsp4.readFile(requestFile, "utf-8");
943
1341
  const request = shellRequestSchema.parse(JSON.parse(raw.trim()));
944
- void appendDebugLog("client", "request parsed", request);
1342
+ void appendDebugLog("client", "request parsed", {
1343
+ lbufferLength: request.lbuffer.length,
1344
+ rbufferLength: request.rbuffer.length,
1345
+ cwd: request.cwd,
1346
+ shellPid: request.shellPid
1347
+ });
945
1348
  const detectedProvider = await detectProvider();
946
1349
  void appendDebugLog("client", "provider detected", { kind: detectedProvider.kind });
947
1350
  if (detectedProvider.kind === "none") {
@@ -967,8 +1370,8 @@ ${detectedProvider.message}\r
967
1370
  });
968
1371
  void appendDebugLog("client", "wrote replace-buffer result", {
969
1372
  resultFile,
970
- lbuffer: fixtureLbuffer,
971
- rbuffer: request.rbuffer
1373
+ lbufferLength: fixtureLbuffer.length,
1374
+ rbufferLength: request.rbuffer.length
972
1375
  });
973
1376
  } else if (resultMode === "llm") {
974
1377
  let ttyReadStream;
@@ -988,6 +1391,8 @@ ${detectedProvider.message}\r
988
1391
  void appendDebugLog("client", "context gathered", {
989
1392
  extraCount: envelope.extras.length
990
1393
  });
1394
+ const adapter = resolveAdapter(detectedProvider);
1395
+ void appendDebugLog("client", "provider adapter resolved", { kind: detectedProvider.kind });
991
1396
  if (ttyHandle) {
992
1397
  try {
993
1398
  ttyReadStream = new tty.ReadStream(ttyHandle.fd);
@@ -1048,7 +1453,8 @@ ${detectedProvider.message}\r
1048
1453
  resolved = true;
1049
1454
  await writeShellResult(resultFile, { kind: "cancel" });
1050
1455
  unmount?.();
1051
- }
1456
+ },
1457
+ onLayoutLinesChange: inZellij ? syncZellijFloatingPaneHeight : void 0
1052
1458
  });
1053
1459
  const renderOptions = ttyReadStream && ttyWriteStream ? { stdin: ttyReadStream, stdout: ttyWriteStream, interactive: true } : { interactive: true };
1054
1460
  const app = render(buildCandidateElement(null), renderOptions);
@@ -1074,7 +1480,7 @@ ${detectedProvider.message}\r
1074
1480
  if (!skipClear) clearScrollReserve();
1075
1481
  resolve3();
1076
1482
  };
1077
- fetchCandidates(envelope, request.rbuffer).then((candidates) => {
1483
+ adapter.fetchCandidates(envelope).then((candidates) => {
1078
1484
  void appendDebugLog("client", "candidates received", { count: candidates.length });
1079
1485
  app.rerender(buildCandidateElement(candidates));
1080
1486
  }).catch(async (err) => {
@@ -1129,32 +1535,32 @@ async function clientCommand(options) {
1129
1535
  import * as net2 from "net";
1130
1536
 
1131
1537
  // src/contracts/ipc.ts
1132
- import { z as z4 } from "zod";
1133
- var ipcRequestSchema = z4.discriminatedUnion("kind", [
1134
- z4.object({
1135
- kind: z4.literal("ping")
1538
+ import { z as z5 } from "zod";
1539
+ var ipcRequestSchema = z5.discriminatedUnion("kind", [
1540
+ z5.object({
1541
+ kind: z5.literal("ping")
1136
1542
  }),
1137
- z4.object({
1138
- kind: z4.literal("ensure-session"),
1139
- shellPid: z4.number().int().positive(),
1140
- ttyPath: z4.string()
1543
+ z5.object({
1544
+ kind: z5.literal("ensure-session"),
1545
+ shellPid: z5.number().int().positive(),
1546
+ ttyPath: z5.string()
1141
1547
  }),
1142
- z4.object({
1143
- kind: z4.literal("run-query"),
1548
+ z5.object({
1549
+ kind: z5.literal("run-query"),
1144
1550
  request: shellRequestSchema
1145
1551
  })
1146
1552
  ]);
1147
- var ipcResponseSchema = z4.discriminatedUnion("kind", [
1148
- z4.object({
1149
- kind: z4.literal("pong")
1553
+ var ipcResponseSchema = z5.discriminatedUnion("kind", [
1554
+ z5.object({
1555
+ kind: z5.literal("pong")
1150
1556
  }),
1151
- z4.object({
1152
- kind: z4.literal("session-ready"),
1153
- socketPath: z4.string()
1557
+ z5.object({
1558
+ kind: z5.literal("session-ready"),
1559
+ socketPath: z5.string()
1154
1560
  }),
1155
- z4.object({
1156
- kind: z4.literal("query-accepted"),
1157
- requestId: z4.string()
1561
+ z5.object({
1562
+ kind: z5.literal("query-accepted"),
1563
+ requestId: z5.string()
1158
1564
  })
1159
1565
  ]);
1160
1566
 
@@ -1248,12 +1654,11 @@ async function daemonCommand(options) {
1248
1654
  }
1249
1655
 
1250
1656
  // src/cli/commands/init.ts
1251
- import { appendFileSync, existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
1252
- import { homedir as homedir2 } from "os";
1253
- import { dirname as dirname3, join as join2, resolve as resolve2 } from "path";
1657
+ import { appendFileSync, existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
1658
+ import { homedir as homedir3 } from "os";
1659
+ import { dirname as dirname3, join as join3, resolve as resolve2 } from "path";
1254
1660
  import { fileURLToPath as fileURLToPath2 } from "url";
1255
1661
  var __dirname = dirname3(fileURLToPath2(import.meta.url));
1256
- var SUPPORTED_SHELLS = ["zsh"];
1257
1662
  var HOMEBREW_PREFIXES = ["/opt/homebrew", "/usr/local", "/home/linuxbrew/.linuxbrew"];
1258
1663
  function _resolveScriptPath(shell) {
1259
1664
  for (const prefix of HOMEBREW_PREFIXES) {
@@ -1262,17 +1667,25 @@ function _resolveScriptPath(shell) {
1262
1667
  }
1263
1668
  return resolve2(__dirname, "../../shell", shell, `queque.${shell}`);
1264
1669
  }
1670
+ function hasShellIntegration(existing, shell) {
1671
+ if (/# queque shell integration/.test(existing)) {
1672
+ return true;
1673
+ }
1674
+ const scriptPattern = new RegExp(`source\\s+["'][^"']*queque\\.${shell.replace(".", "\\.")}["']`);
1675
+ return scriptPattern.test(existing);
1676
+ }
1265
1677
  function initCommand(shell) {
1266
- if (!SUPPORTED_SHELLS.includes(shell)) {
1267
- console.error(`Unsupported shell: '${shell}'. Supported: ${SUPPORTED_SHELLS.join(", ")}`);
1678
+ bootstrapBuiltins();
1679
+ const supportedShells = listShellAdapters().map((adapter) => adapter.shell);
1680
+ if (!supportedShells.includes(shell)) {
1681
+ console.error(`Unsupported shell: '${shell}'. Supported: ${supportedShells.join(", ")}`);
1268
1682
  process.exit(1);
1269
1683
  }
1270
- const zshrc = join2(homedir2(), ".zshrc");
1271
- const marker = /\bqueque\b/;
1684
+ const zshrc = join3(homedir3(), ".zshrc");
1272
1685
  const scriptPath = _resolveScriptPath(shell);
1273
1686
  const snippet = `source "${scriptPath}"`;
1274
- const existing = existsSync3(zshrc) ? readFileSync2(zshrc, "utf8") : "";
1275
- if (existing.search(marker)) {
1687
+ const existing = existsSync3(zshrc) ? readFileSync3(zshrc, "utf8") : "";
1688
+ if (hasShellIntegration(existing, shell)) {
1276
1689
  console.error("queque: shell integration already present in ~/.zshrc");
1277
1690
  process.exit(0);
1278
1691
  }