@k-leumas/queque-cli 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.
@@ -0,0 +1,1265 @@
1
+ // src/cli/main.ts
2
+ import * as fs2 from "fs";
3
+ import { cac } from "cac";
4
+
5
+ // src/context/providers/filesystem-context.ts
6
+ var FILENAME_RE = /\b[\w.-]+\.\w{2,6}\b/g;
7
+ var filesystemContextProvider = {
8
+ id: "filesystem-context",
9
+ intents: ["filesystem"],
10
+ async gather(input) {
11
+ const match = input.base.queryText.match(FILENAME_RE);
12
+ return {
13
+ kind: "filesystem",
14
+ payload: {
15
+ cwd: input.base.cwd,
16
+ apparentFilename: match?.[0] ?? null
17
+ }
18
+ };
19
+ }
20
+ };
21
+
22
+ // src/context/providers/git-context.ts
23
+ import { execFile as execFile2 } from "child_process";
24
+ import { statSync } from "fs";
25
+ import { promisify as promisify2 } from "util";
26
+
27
+ // src/shared/debug-log.ts
28
+ import * as fsp from "fs/promises";
29
+ var debugLogPath = process.env.QQ_DEBUG_LOG_FILE ?? `/tmp/qq-${process.getuid?.() ?? "unknown"}-debug.log`;
30
+ function formatDetails(details) {
31
+ if (details === void 0) return "";
32
+ try {
33
+ return ` ${JSON.stringify(details)}`;
34
+ } catch {
35
+ return " [unserializable-details]";
36
+ }
37
+ }
38
+ async function appendDebugLog(scope, message, details) {
39
+ const line = `${(/* @__PURE__ */ new Date()).toISOString()} [${scope}] ${message}${formatDetails(details)}
40
+ `;
41
+ try {
42
+ await fsp.appendFile(debugLogPath, line, { encoding: "utf-8", mode: 384 });
43
+ } catch {
44
+ }
45
+ }
46
+
47
+ // src/shared/vcs-context.ts
48
+ import { execFile } from "child_process";
49
+ import { promisify } from "util";
50
+ var execFileAsync = promisify(execFile);
51
+ async function runGit(cwd, args) {
52
+ try {
53
+ const { stdout } = await execFileAsync("git", ["-C", cwd, ...args], { encoding: "utf8" });
54
+ return stdout.trim();
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+ async function detectVcsContext(cwd) {
60
+ const insideWorkTree = await runGit(cwd, ["rev-parse", "--is-inside-work-tree"]);
61
+ if (insideWorkTree !== "true") {
62
+ return { kind: "none", cwd };
63
+ }
64
+ const root = await runGit(cwd, ["rev-parse", "--show-toplevel"]) ?? cwd;
65
+ const branch = await runGit(cwd, ["branch", "--show-current"]) || null;
66
+ const dirty = Boolean((await runGit(cwd, ["status", "--porcelain=v1"]))?.length);
67
+ return {
68
+ kind: "git",
69
+ cwd,
70
+ root,
71
+ branch,
72
+ dirty
73
+ };
74
+ }
75
+
76
+ // src/context/providers/git-context.ts
77
+ var execFileAsync2 = promisify2(execFile2);
78
+ var GIT_TIMEOUT_MS = 5e3;
79
+ function assertSafeCwd(cwd) {
80
+ let st;
81
+ try {
82
+ st = statSync(cwd);
83
+ } catch {
84
+ throw new Error(`cwd not accessible: ${cwd}`);
85
+ }
86
+ if (!st.isDirectory()) throw new Error(`cwd is not a directory: ${cwd}`);
87
+ }
88
+ function unescapeGitPath(s) {
89
+ return s.replace(/\\([0-7]{3})/g, (_, oct) => String.fromCodePoint(parseInt(oct, 8))).replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\\\/g, "\\");
90
+ }
91
+ function stripQuotes(value) {
92
+ if (value.startsWith('"') && value.endsWith('"')) {
93
+ return unescapeGitPath(value.slice(1, -1));
94
+ }
95
+ return value;
96
+ }
97
+ function parsePorcelainLine(line) {
98
+ if (line.length < 4) {
99
+ return null;
100
+ }
101
+ const rest = line.slice(3);
102
+ const renameSeparator = " -> ";
103
+ const renameIndex = rest.indexOf(renameSeparator);
104
+ if (renameIndex !== -1) {
105
+ return stripQuotes(rest.slice(renameIndex + renameSeparator.length));
106
+ }
107
+ return stripQuotes(rest);
108
+ }
109
+ async function getChangedFiles(cwd) {
110
+ try {
111
+ assertSafeCwd(cwd);
112
+ const { stdout } = await execFileAsync2("git", ["-C", cwd, "status", "--porcelain"], {
113
+ encoding: "utf8",
114
+ timeout: GIT_TIMEOUT_MS
115
+ });
116
+ return stdout.split("\n").filter(Boolean).map(parsePorcelainLine).filter((path2) => path2 !== null);
117
+ } catch {
118
+ return [];
119
+ }
120
+ }
121
+ var gitContextProvider = {
122
+ id: "git-context",
123
+ intents: ["codebase", "shell-command"],
124
+ async gather(input) {
125
+ if (input.decision.intent === "shell-command" && !input.decision.signals.includes("git-prefix")) {
126
+ return null;
127
+ }
128
+ try {
129
+ const vcs = await detectVcsContext(input.base.cwd);
130
+ if (vcs.kind === "none") {
131
+ return null;
132
+ }
133
+ const changedFiles = await getChangedFiles(input.base.cwd);
134
+ void appendDebugLog("context", "git provider gathered", {
135
+ cwd: input.base.cwd,
136
+ branch: vcs.branch,
137
+ dirty: vcs.dirty,
138
+ changedFiles: changedFiles.length
139
+ });
140
+ return {
141
+ kind: "git",
142
+ payload: {
143
+ cwd: vcs.cwd,
144
+ root: vcs.root,
145
+ branch: vcs.branch,
146
+ dirty: vcs.dirty,
147
+ changedFiles
148
+ }
149
+ };
150
+ } catch (error) {
151
+ void appendDebugLog("context", "git provider failed", {
152
+ cwd: input.base.cwd,
153
+ message: error instanceof Error ? error.message : String(error)
154
+ });
155
+ return null;
156
+ }
157
+ }
158
+ };
159
+
160
+ // src/registry/context-providers.ts
161
+ var registry = /* @__PURE__ */ new Map();
162
+ function registerContextProvider(descriptor) {
163
+ if (registry.has(descriptor.id)) {
164
+ throw new Error(`Context provider already registered: "${descriptor.id}"`);
165
+ }
166
+ registry.set(descriptor.id, descriptor);
167
+ }
168
+ function listContextProviders() {
169
+ return Array.from(registry.values());
170
+ }
171
+
172
+ // src/registry/provider-backends.ts
173
+ var registry2 = /* @__PURE__ */ new Map();
174
+ function registerProviderBackend(descriptor) {
175
+ if (registry2.has(descriptor.id)) {
176
+ throw new Error(`Provider backend already registered: "${descriptor.id}"`);
177
+ }
178
+ registry2.set(descriptor.id, descriptor);
179
+ }
180
+
181
+ // src/registry/shell-adapters.ts
182
+ var registry3 = /* @__PURE__ */ new Map();
183
+ function registerShellAdapter(descriptor) {
184
+ if (registry3.has(descriptor.id)) {
185
+ throw new Error(`Shell adapter already registered: "${descriptor.id}"`);
186
+ }
187
+ registry3.set(descriptor.id, descriptor);
188
+ }
189
+
190
+ // src/registry/storage-hooks.ts
191
+ var registry4 = /* @__PURE__ */ new Map();
192
+ function registerStorageHook(descriptor) {
193
+ if (registry4.has(descriptor.id)) {
194
+ throw new Error(`Storage hook already registered: "${descriptor.id}"`);
195
+ }
196
+ registry4.set(descriptor.id, descriptor);
197
+ }
198
+
199
+ // src/registry/bootstrap.ts
200
+ var bootstrapped = false;
201
+ function bootstrapBuiltins() {
202
+ if (bootstrapped) {
203
+ return;
204
+ }
205
+ bootstrapped = true;
206
+ registerContextProvider(gitContextProvider);
207
+ registerContextProvider(filesystemContextProvider);
208
+ registerShellAdapter({
209
+ id: "zsh",
210
+ shell: "zsh",
211
+ description: "zsh ZLE widget adapter (Phase 2 built-in)"
212
+ });
213
+ registerStorageHook({
214
+ id: "noop",
215
+ description: "No-op storage hook (default \u2014 no persistence)"
216
+ });
217
+ registerStorageHook({
218
+ id: "memory",
219
+ description: "In-memory storage hook (test and ephemeral use)"
220
+ });
221
+ registerProviderBackend({
222
+ id: "claude",
223
+ name: "Claude (Anthropic)",
224
+ description: "Anthropic Claude adapter \u2014 default LLM backend"
225
+ });
226
+ }
227
+
228
+ // src/client/run-foreground.ts
229
+ import * as fsp4 from "fs/promises";
230
+ import * as tty from "tty";
231
+ import { render } from "ink";
232
+ import React from "react";
233
+
234
+ // src/contracts/request.ts
235
+ import { z as z2 } from "zod";
236
+
237
+ // src/contracts/shell.ts
238
+ import { z } from "zod";
239
+ var shellRequestSchema = z.object({
240
+ version: z.literal(1),
241
+ ttyPath: z.string(),
242
+ cwd: z.string().regex(/^\/[^\0]*$/, "cwd must be an absolute POSIX path"),
243
+ shellPid: z.number().int().positive(),
244
+ lbuffer: z.string(),
245
+ rbuffer: z.string()
246
+ });
247
+ var shellResultSchema = z.discriminatedUnion("kind", [
248
+ z.object({
249
+ kind: z.literal("cancel")
250
+ }).strict(),
251
+ z.object({
252
+ kind: z.literal("replace-buffer"),
253
+ lbuffer: z.string(),
254
+ rbuffer: z.string(),
255
+ query: z.string().optional()
256
+ }),
257
+ z.object({
258
+ kind: z.literal("error"),
259
+ message: z.string()
260
+ })
261
+ ]);
262
+
263
+ // src/contracts/request.ts
264
+ var requestIntentSchema = z2.enum([
265
+ "shell-command",
266
+ "codebase",
267
+ "filesystem",
268
+ "general",
269
+ "unknown"
270
+ ]);
271
+ var normalizedRequestSchema = shellRequestSchema.extend({
272
+ intent: requestIntentSchema,
273
+ confidence: z2.number().min(0).max(1)
274
+ });
275
+ var baseContextSchema = z2.object({
276
+ queryText: z2.string(),
277
+ cwd: z2.string(),
278
+ ttyPath: z2.string(),
279
+ shellPid: z2.number().int().positive(),
280
+ /** Hardcoded 'zsh' for Phase 2 — cross-shell support is v2 scope. */
281
+ shellName: z2.literal("zsh"),
282
+ platform: z2.string(),
283
+ timestamp: z2.string()
284
+ });
285
+ var intentDecisionSchema = z2.object({
286
+ intent: requestIntentSchema,
287
+ confidence: z2.number().min(0).max(1),
288
+ signals: z2.array(z2.string())
289
+ });
290
+ var contextChunkSchema = z2.discriminatedUnion("kind", [
291
+ z2.object({
292
+ kind: z2.literal("git"),
293
+ payload: z2.object({
294
+ cwd: z2.string(),
295
+ root: z2.string(),
296
+ branch: z2.string().nullable(),
297
+ dirty: z2.boolean(),
298
+ /** File paths only (from git status --porcelain). No diff content, no file bytes. */
299
+ changedFiles: z2.array(z2.string())
300
+ })
301
+ }),
302
+ z2.object({
303
+ kind: z2.literal("filesystem"),
304
+ payload: z2.object({
305
+ cwd: z2.string(),
306
+ /** Apparent filename extracted from query text. No file bytes read. */
307
+ apparentFilename: z2.string().nullable()
308
+ })
309
+ })
310
+ ]);
311
+ var contextEnvelopeSchema = z2.object({
312
+ base: baseContextSchema,
313
+ extras: z2.array(contextChunkSchema)
314
+ });
315
+
316
+ // src/intent/router.ts
317
+ var GIT_PREFIX_RE = /^git\s/;
318
+ var PKG_MANAGER_RE = /^(npm|pnpm|yarn)\s+(test|build|lint|run|start|dev|check|format|typecheck)\b/i;
319
+ var FILESYSTEM_KEYWORDS_RE = /\b(?:rename|move(?:\s+file)?|find\s+files?|copy|delete|list\s+files?|image|folder|directory)\b/i;
320
+ var SHELL_COMMAND_RE = /^(ls|echo|grep|awk|sed|curl|wget|ssh|cat|head|tail|cd|pwd|mkdir|touch|chmod|cp|mv|rm|which|man|ps|kill|sudo|command)\s|\|/;
321
+ var FILE_PATH_RE = /(?:\/|\.\/|~\/|\.[a-zA-Z][\w-]*(?:\.\w+)?(?=\s|$)|\b[\w-]+\.(?:ts|tsx|js|jsx|mjs|cjs|py|go|rs|rb|java|sh|md|json|yaml|yml|toml|env|html|css|scss|txt|xml|svg|png|jpg|jpeg|gif|pdf|zip|tar|gz|lock|config)(?:\b|$))/;
322
+ function classifyIntent(request) {
323
+ const query = request.lbuffer.trim();
324
+ if (query.length === 0) {
325
+ return {
326
+ intent: requestIntentSchema.enum.unknown,
327
+ confidence: 1,
328
+ signals: ["empty-query"]
329
+ };
330
+ }
331
+ if (GIT_PREFIX_RE.test(query)) {
332
+ return {
333
+ intent: requestIntentSchema.enum["shell-command"],
334
+ confidence: 1,
335
+ signals: ["git-prefix"]
336
+ };
337
+ }
338
+ if (PKG_MANAGER_RE.test(query)) {
339
+ return {
340
+ intent: requestIntentSchema.enum.codebase,
341
+ confidence: 1,
342
+ signals: ["pkg-manager-script"]
343
+ };
344
+ }
345
+ if (FILESYSTEM_KEYWORDS_RE.test(query)) {
346
+ return {
347
+ intent: requestIntentSchema.enum.filesystem,
348
+ confidence: 0.85,
349
+ signals: ["filesystem-keyword"]
350
+ };
351
+ }
352
+ if (SHELL_COMMAND_RE.test(query)) {
353
+ return {
354
+ intent: requestIntentSchema.enum["shell-command"],
355
+ confidence: 0.8,
356
+ signals: ["shell-command-signal"]
357
+ };
358
+ }
359
+ if (FILE_PATH_RE.test(query)) {
360
+ return {
361
+ intent: requestIntentSchema.enum.codebase,
362
+ confidence: 0.9,
363
+ signals: ["file-path-signal"]
364
+ };
365
+ }
366
+ return {
367
+ intent: requestIntentSchema.enum.general,
368
+ confidence: 0.5,
369
+ signals: ["no-strong-signal"]
370
+ };
371
+ }
372
+
373
+ // src/context/base-context.ts
374
+ function buildBaseContext(request) {
375
+ return {
376
+ queryText: request.lbuffer.trim(),
377
+ cwd: request.cwd,
378
+ ttyPath: request.ttyPath,
379
+ shellPid: request.shellPid,
380
+ shellName: "zsh",
381
+ platform: process.platform,
382
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
383
+ };
384
+ }
385
+
386
+ // src/context/pipeline.ts
387
+ async function gatherContext(request) {
388
+ void appendDebugLog("context", "pipeline start", {
389
+ lbuffer: request.lbuffer,
390
+ intent: request.intent
391
+ });
392
+ const base = buildBaseContext(request);
393
+ const decision = classifyIntent(request);
394
+ void appendDebugLog("context", "intent decision", {
395
+ intent: decision.intent,
396
+ signals: decision.signals
397
+ });
398
+ const extras = [];
399
+ const providers = listContextProviders();
400
+ for (const provider of providers) {
401
+ const supportsAllIntents = provider.intents.includes("*");
402
+ const supportsIntent = provider.intents.includes(decision.intent);
403
+ if (!supportsAllIntents && !supportsIntent) {
404
+ continue;
405
+ }
406
+ try {
407
+ const chunk = await provider.gather({ base, decision });
408
+ if (chunk !== null) {
409
+ extras.push(chunk);
410
+ void appendDebugLog("context", "provider chunk gathered", {
411
+ providerId: provider.id,
412
+ kind: chunk.kind
413
+ });
414
+ }
415
+ } catch (error) {
416
+ void appendDebugLog("context", "provider error", {
417
+ providerId: provider.id,
418
+ message: error instanceof Error ? error.message : String(error)
419
+ });
420
+ }
421
+ }
422
+ const envelope = { base, extras };
423
+ void appendDebugLog("context", "envelope assembled", { extraCount: extras.length });
424
+ return envelope;
425
+ }
426
+
427
+ // src/daemon/bootstrap.ts
428
+ import { spawn } from "child_process";
429
+ import * as fs from "fs";
430
+ import * as net from "net";
431
+ import * as path from "path";
432
+ import { fileURLToPath } from "url";
433
+ var CONNECT_TIMEOUT_MS = 500;
434
+ var POLL_INTERVAL_MS = 50;
435
+ var POLL_MAX_ATTEMPTS = 40;
436
+ function tryConnect(socketPath2) {
437
+ return new Promise((resolve2) => {
438
+ const timer = setTimeout(() => {
439
+ client.destroy();
440
+ resolve2(false);
441
+ }, CONNECT_TIMEOUT_MS);
442
+ const client = net.createConnection(socketPath2, () => {
443
+ clearTimeout(timer);
444
+ client.destroy();
445
+ resolve2(true);
446
+ });
447
+ client.on("error", () => {
448
+ clearTimeout(timer);
449
+ resolve2(false);
450
+ });
451
+ });
452
+ }
453
+ async function waitForSocket(socketPath2, maxAttempts = POLL_MAX_ATTEMPTS) {
454
+ for (let i = 0; i < maxAttempts; i++) {
455
+ if (await tryConnect(socketPath2)) return true;
456
+ await new Promise((res) => setTimeout(res, POLL_INTERVAL_MS));
457
+ }
458
+ return false;
459
+ }
460
+ function assertSafeSocketPath(socketPath2) {
461
+ const resolved = path.resolve(socketPath2);
462
+ const base = path.basename(resolved);
463
+ const dir = path.dirname(resolved);
464
+ const tmpRoots = ["/tmp", "/private/tmp"];
465
+ if (!tmpRoots.includes(dir) || !base.startsWith("qq-") || !base.endsWith(".sock")) {
466
+ throw new Error(`unsafe socket path rejected: ${socketPath2}`);
467
+ }
468
+ }
469
+ async function ensureDaemon(socketPath2) {
470
+ assertSafeSocketPath(socketPath2);
471
+ void appendDebugLog("daemon", "ensure start", { socketPath: socketPath2 });
472
+ if (await tryConnect(socketPath2)) {
473
+ void appendDebugLog("daemon", "already running", { socketPath: socketPath2 });
474
+ return;
475
+ }
476
+ try {
477
+ fs.unlinkSync(socketPath2);
478
+ } catch {
479
+ }
480
+ const qqBin = process.execPath;
481
+ const qqScript = fileURLToPath(import.meta.url);
482
+ if (!fs.existsSync(qqScript)) {
483
+ void appendDebugLog("daemon", "script missing", { qqScript });
484
+ throw new Error(`qq daemon script not found at: ${qqScript}. Run 'pnpm build' first.`);
485
+ }
486
+ void appendDebugLog("daemon", "spawning daemon", { qqScript, socketPath: socketPath2 });
487
+ const child = spawn(qqBin, [qqScript, "daemon", "--socket", socketPath2], {
488
+ detached: true,
489
+ stdio: "ignore"
490
+ });
491
+ child.unref();
492
+ const started = await waitForSocket(socketPath2);
493
+ if (!started) {
494
+ void appendDebugLog("daemon", "startup timeout", { socketPath: socketPath2 });
495
+ throw new Error(`qq daemon did not start within the expected window (${socketPath2})`);
496
+ }
497
+ void appendDebugLog("daemon", "daemon ready", { socketPath: socketPath2 });
498
+ }
499
+
500
+ // src/providers/claude.ts
501
+ import Anthropic from "@anthropic-ai/sdk";
502
+
503
+ // src/contracts/candidates.ts
504
+ import { z as z3 } from "zod";
505
+ var commandCandidateSchema = z3.object({
506
+ command: z3.string().min(1),
507
+ explanation: z3.string()
508
+ });
509
+ var candidateListSchema = z3.array(commandCandidateSchema).min(1).max(5);
510
+
511
+ // src/shared/env-file.ts
512
+ import { existsSync as existsSync2, readFileSync } from "fs";
513
+ import { dirname as dirname2, join } from "path";
514
+ import { fileURLToPath as fileURLToPath2 } from "url";
515
+ function findUp(startDir, filename) {
516
+ let current = startDir;
517
+ while (true) {
518
+ const candidate = join(current, filename);
519
+ if (existsSync2(candidate)) {
520
+ return candidate;
521
+ }
522
+ const parent = dirname2(current);
523
+ if (parent === current) {
524
+ return null;
525
+ }
526
+ current = parent;
527
+ }
528
+ }
529
+ function parseEnvFile(content) {
530
+ const values = /* @__PURE__ */ new Map();
531
+ for (const rawLine of content.split(/\r?\n/)) {
532
+ const line = rawLine.trim();
533
+ if (!line || line.startsWith("#")) {
534
+ continue;
535
+ }
536
+ const match = line.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
537
+ if (!match) {
538
+ continue;
539
+ }
540
+ const [, key, rawValue] = match;
541
+ let value = rawValue.trim();
542
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
543
+ value = value.slice(1, -1);
544
+ }
545
+ values.set(key, value);
546
+ }
547
+ return values;
548
+ }
549
+ var envCache = /* @__PURE__ */ new Map();
550
+ function readEnvValueFromDotEnvLocal(key, startDir = dirname2(fileURLToPath2(import.meta.url))) {
551
+ const cacheKey = `${key}\0${startDir}`;
552
+ if (envCache.has(cacheKey)) {
553
+ return envCache.get(cacheKey) ?? null;
554
+ }
555
+ const envPath = findUp(startDir, ".env.local");
556
+ if (!envPath) {
557
+ envCache.set(cacheKey, null);
558
+ return null;
559
+ }
560
+ const values = parseEnvFile(readFileSync(envPath, "utf8"));
561
+ const result = values.get(key) ?? null;
562
+ envCache.set(cacheKey, result);
563
+ return result;
564
+ }
565
+
566
+ // src/providers/claude.ts
567
+ var DEFAULT_MODEL = "claude-haiku-4-5-20251001";
568
+ function resolveModel() {
569
+ return process.env.QQ_MODEL ?? readEnvValueFromDotEnvLocal("QQ_MODEL") ?? DEFAULT_MODEL;
570
+ }
571
+ function extractText(content) {
572
+ return content.filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text ?? "").join("").trim();
573
+ }
574
+ function stripCodeFence(raw) {
575
+ const m = raw.trim().match(/^```(?:json)?\s*\n?([\s\S]*?)\n?```\s*$/);
576
+ return m ? m[1].trim() : raw.trim();
577
+ }
578
+ function parseCandidates(text) {
579
+ let parsed;
580
+ try {
581
+ parsed = JSON.parse(stripCodeFence(text));
582
+ } catch {
583
+ const trimmed = text.trim();
584
+ return [{ command: trimmed || 'echo ""', explanation: "" }];
585
+ }
586
+ try {
587
+ return candidateListSchema.parse(parsed);
588
+ } catch {
589
+ return [{ command: 'echo ""', explanation: "QueQue: unexpected response format" }];
590
+ }
591
+ }
592
+ function shouldForceSelector() {
593
+ const configured = process.env.QQ_FORCE_SELECTOR ?? readEnvValueFromDotEnvLocal("QQ_FORCE_SELECTOR");
594
+ return configured === "1" || configured === "true";
595
+ }
596
+ function ensureSelectableCandidates(candidates) {
597
+ if (!shouldForceSelector() || candidates.length >= 2) {
598
+ return candidates;
599
+ }
600
+ const [first] = candidates;
601
+ return [
602
+ first,
603
+ {
604
+ command: first.command,
605
+ explanation: first.explanation.length > 0 ? `${first.explanation} (forced duplicate for selector testing)` : "Forced duplicate for selector testing."
606
+ }
607
+ ];
608
+ }
609
+ function buildPrompt(envelope) {
610
+ const gitChunk = envelope.extras.find((chunk) => chunk.kind === "git");
611
+ return [
612
+ "Return ONLY a JSON array of 1-3 shell command candidates, most likely first.",
613
+ 'Each item must have exactly these keys: "command" and "explanation".',
614
+ "No markdown, no prose, no code fences. Raw JSON array only.",
615
+ "",
616
+ "The command should be safe to place back into a shell buffer.",
617
+ "",
618
+ "Shell context:",
619
+ JSON.stringify(
620
+ {
621
+ cwd: envelope.base.cwd,
622
+ queryText: envelope.base.queryText,
623
+ platform: envelope.base.platform,
624
+ shellName: envelope.base.shellName,
625
+ ...gitChunk ? { versionControl: gitChunk.payload } : {}
626
+ },
627
+ null,
628
+ 2
629
+ )
630
+ ].join("\n");
631
+ }
632
+ async function fetchCandidates(envelope, rbuffer = "") {
633
+ const apiKey = process.env.ANTHROPIC_API_KEY ?? readEnvValueFromDotEnvLocal("ANTHROPIC_API_KEY");
634
+ if (!apiKey) {
635
+ throw new Error("ANTHROPIC_API_KEY is required in the environment or .env.local");
636
+ }
637
+ const client = new Anthropic({ apiKey });
638
+ const prompt = buildPrompt(envelope);
639
+ const model = resolveModel();
640
+ void appendDebugLog("provider", "request start", {
641
+ model,
642
+ cwd: envelope.base.cwd,
643
+ rbufferLength: rbuffer.length,
644
+ extras: envelope.extras.map((chunk) => chunk.kind)
645
+ });
646
+ try {
647
+ const response = await client.messages.create(
648
+ {
649
+ model,
650
+ max_tokens: 1024,
651
+ temperature: 0,
652
+ 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.",
653
+ messages: [{ role: "user", content: prompt }]
654
+ },
655
+ {
656
+ timeout: 25e3
657
+ // 25s — slightly under the zsh 30s FIFO timeout
658
+ }
659
+ );
660
+ const text = extractText(response.content);
661
+ const candidates = ensureSelectableCandidates(parseCandidates(text));
662
+ void appendDebugLog("provider", "response parsed", {
663
+ model,
664
+ candidateCount: candidates.length,
665
+ forceSelector: shouldForceSelector()
666
+ });
667
+ return candidates;
668
+ } catch (error) {
669
+ void appendDebugLog("provider", "request failed", {
670
+ model,
671
+ message: error instanceof Error ? error.message : String(error)
672
+ });
673
+ throw error;
674
+ }
675
+ }
676
+
677
+ // src/providers/detect.ts
678
+ import { execSync } from "child_process";
679
+ import * as fsp2 from "fs/promises";
680
+ import * as os from "os";
681
+ function claudeOnPath() {
682
+ try {
683
+ execSync("which claude", { stdio: "pipe" });
684
+ return true;
685
+ } catch {
686
+ return false;
687
+ }
688
+ }
689
+ async function claudeAuthFileExists() {
690
+ const home = os.homedir();
691
+ const candidates = [`${home}/.claude/.credentials.json`, `${home}/.claude/credentials.json`];
692
+ for (const p of candidates) {
693
+ try {
694
+ await fsp2.stat(p);
695
+ return true;
696
+ } catch {
697
+ }
698
+ }
699
+ return false;
700
+ }
701
+ async function ollamaHealthy() {
702
+ try {
703
+ const res = await fetch("http://localhost:11434/api/tags", {
704
+ signal: AbortSignal.timeout(300)
705
+ });
706
+ return res.ok;
707
+ } catch {
708
+ return false;
709
+ }
710
+ }
711
+ async function detectProvider() {
712
+ if (process.env.ANTHROPIC_API_KEY) {
713
+ return { kind: "anthropic-key" };
714
+ }
715
+ if (claudeOnPath() && await claudeAuthFileExists()) {
716
+ return { kind: "claude-cli" };
717
+ }
718
+ if (await ollamaHealthy()) {
719
+ return { kind: "ollama", baseUrl: "http://localhost:11434" };
720
+ }
721
+ if (process.env.OPENAI_API_KEY) {
722
+ return { kind: "openai-key" };
723
+ }
724
+ return { kind: "none" };
725
+ }
726
+
727
+ // src/shared/socket-path.ts
728
+ function socketPathForUid(uid) {
729
+ return `/tmp/qq-${uid}.sock`;
730
+ }
731
+ function socketPath() {
732
+ return socketPathForUid(process.getuid?.() ?? 0);
733
+ }
734
+
735
+ // src/ui/CandidateSelect.tsx
736
+ import { Box as Box4, Text as Text4, useInput } from "ink";
737
+ import { useEffect as useEffect2, useState as useState2 } from "react";
738
+
739
+ // src/ui/ControlsLine.tsx
740
+ import { Box, Text } from "ink";
741
+ import { jsx, jsxs } from "react/jsx-runtime";
742
+ function ControlsLine() {
743
+ return /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
744
+ /* @__PURE__ */ jsx(Text, { backgroundColor: "ansi256(238)", color: "white", bold: true, children: " \u2191\u2193 " }),
745
+ /* @__PURE__ */ jsx(Text, { backgroundColor: "ansi256(245)", color: "black", children: " select " }),
746
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " }),
747
+ /* @__PURE__ */ jsx(Text, { backgroundColor: "ansi256(238)", color: "white", bold: true, children: " enter " }),
748
+ /* @__PURE__ */ jsx(Text, { backgroundColor: "ansi256(245)", color: "black", children: " accept " }),
749
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " }),
750
+ /* @__PURE__ */ jsx(Text, { backgroundColor: "ansi256(238)", color: "white", bold: true, children: " esc " }),
751
+ /* @__PURE__ */ jsx(Text, { backgroundColor: "ansi256(245)", color: "black", children: " cancel " })
752
+ ] });
753
+ }
754
+
755
+ // src/ui/LoadingSpinner.tsx
756
+ import { Text as Text2 } from "ink";
757
+ import { useEffect, useState } from "react";
758
+ import { jsx as jsx2 } from "react/jsx-runtime";
759
+ var FRAMES = ["thinking\u2026", "thinking.", "thinking..", "thinking\u2026"];
760
+ function LoadingSpinner() {
761
+ const [frame, setFrame] = useState(0);
762
+ useEffect(() => {
763
+ const id = setInterval(() => {
764
+ setFrame((f) => (f + 1) % FRAMES.length);
765
+ }, 200);
766
+ return () => clearInterval(id);
767
+ }, []);
768
+ return /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: FRAMES[frame] });
769
+ }
770
+
771
+ // src/ui/Modal.tsx
772
+ import { Box as Box2 } from "ink";
773
+ import { jsx as jsx3 } from "react/jsx-runtime";
774
+ function Modal({ children, width = 80 }) {
775
+ return /* @__PURE__ */ jsx3(Box2, { flexDirection: "column", paddingX: 1, width, children });
776
+ }
777
+
778
+ // src/ui/SearchInput.tsx
779
+ import { Box as Box3, Text as Text3 } from "ink";
780
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
781
+ function SearchInput({ query }) {
782
+ return /* @__PURE__ */ jsxs2(Box3, { children: [
783
+ /* @__PURE__ */ jsx4(Text3, { color: "cyan", bold: true, children: "FILTER: " }),
784
+ query ? /* @__PURE__ */ jsx4(Text3, { children: query }) : /* @__PURE__ */ jsx4(Text3, { dimColor: true, children: "type to filter\u2026" })
785
+ ] });
786
+ }
787
+
788
+ // src/ui/CandidateSelect.tsx
789
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
790
+ function filterCandidates(candidates, query) {
791
+ if (!query) return candidates;
792
+ const lower = query.toLowerCase();
793
+ return candidates.filter((c) => c.command.toLowerCase().includes(lower));
794
+ }
795
+ function CandidateSelect({
796
+ candidates,
797
+ initialQuery,
798
+ onSelect,
799
+ onCancel,
800
+ error
801
+ }) {
802
+ const [selectedIndex, setSelectedIndex] = useState2(0);
803
+ const [query, setQuery] = useState2(initialQuery ?? "");
804
+ useEffect2(() => {
805
+ setSelectedIndex(0);
806
+ if (candidates && candidates.length > 0 && filterCandidates(candidates, query).length === 0) {
807
+ setQuery("");
808
+ }
809
+ }, [candidates]);
810
+ useEffect2(() => {
811
+ setSelectedIndex(0);
812
+ }, [query]);
813
+ useInput((input, key) => {
814
+ if (key.escape) {
815
+ onCancel();
816
+ return;
817
+ }
818
+ if (key.backspace) {
819
+ setQuery((q) => q.slice(0, -1));
820
+ return;
821
+ }
822
+ if (key.return && candidates) {
823
+ const visible = filterCandidates(candidates, query);
824
+ if (visible.length === 0) return;
825
+ const selected = visible[selectedIndex] ?? visible[0];
826
+ const explanation = selected.explanation || `see man ${selected.command.split(" ")[0]}`;
827
+ onSelect(selected.command, explanation);
828
+ return;
829
+ }
830
+ if (input && !key.ctrl && !key.meta) {
831
+ setQuery((q) => q + input);
832
+ return;
833
+ }
834
+ if (key.upArrow && candidates) {
835
+ const visible = filterCandidates(candidates, query);
836
+ setSelectedIndex((i) => i === 0 ? visible.length - 1 : i - 1);
837
+ return;
838
+ }
839
+ if (key.downArrow && candidates) {
840
+ const visible = filterCandidates(candidates, query);
841
+ setSelectedIndex((i) => (i + 1) % visible.length);
842
+ return;
843
+ }
844
+ });
845
+ let content;
846
+ if (error === true) {
847
+ content = /* @__PURE__ */ jsx5(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text4, { dimColor: true, children: "provider error \u2014 press esc to return" }) });
848
+ } else if (candidates === null) {
849
+ content = /* @__PURE__ */ jsx5(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx5(LoadingSpinner, {}) });
850
+ } else {
851
+ const visible = filterCandidates(candidates, query);
852
+ if (visible.length === 0) {
853
+ content = /* @__PURE__ */ jsx5(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text4, { dimColor: true, children: "no matches \u2014 try a different query" }) });
854
+ } else {
855
+ content = /* @__PURE__ */ jsx5(Box4, { flexDirection: "column", marginTop: 1, children: visible.map((candidate, index) => {
856
+ const active = index === selectedIndex;
857
+ return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", marginBottom: 1, children: [
858
+ /* @__PURE__ */ jsxs3(Box4, { children: [
859
+ active ? /* @__PURE__ */ jsx5(Text4, { color: "ansi256(166)", children: "\u250C> " }) : /* @__PURE__ */ jsx5(Text4, { children: " " }),
860
+ /* @__PURE__ */ jsx5(Text4, { bold: active, color: active ? "white" : void 0, dimColor: !active, children: candidate.command })
861
+ ] }),
862
+ candidate.explanation.length > 0 && /* @__PURE__ */ jsxs3(Box4, { children: [
863
+ /* @__PURE__ */ jsx5(Text4, { children: " " }),
864
+ /* @__PURE__ */ jsx5(Text4, { dimColor: true, children: candidate.explanation })
865
+ ] })
866
+ ] }, candidate.command);
867
+ }) });
868
+ }
869
+ }
870
+ return /* @__PURE__ */ jsxs3(Modal, { children: [
871
+ initialQuery && initialQuery.length > 0 && /* @__PURE__ */ jsx5(Box4, { children: /* @__PURE__ */ jsx5(Text4, { dimColor: true, children: `queque \u203A ${initialQuery}` }) }),
872
+ /* @__PURE__ */ jsx5(SearchInput, { query }),
873
+ content,
874
+ /* @__PURE__ */ jsx5(ControlsLine, {})
875
+ ] });
876
+ }
877
+
878
+ // src/client/result-writer.ts
879
+ import * as fsp3 from "fs/promises";
880
+ async function writeShellResult(resultFile, result) {
881
+ const parsed = shellResultSchema.parse(result);
882
+ const line = `${JSON.stringify(parsed)}
883
+ `;
884
+ let isFifo = false;
885
+ try {
886
+ const st = await fsp3.stat(resultFile);
887
+ isFifo = st.isFIFO();
888
+ } catch {
889
+ }
890
+ if (isFifo) {
891
+ await fsp3.writeFile(resultFile, line, { encoding: "utf-8" });
892
+ } else {
893
+ const tmpFile = `${resultFile}.tmp`;
894
+ await fsp3.writeFile(tmpFile, line, { encoding: "utf-8" });
895
+ await fsp3.rename(tmpFile, resultFile);
896
+ }
897
+ }
898
+
899
+ // src/client/run-foreground.ts
900
+ function buildShellBuffers(command, explanation) {
901
+ const comment = ` # ${explanation}`;
902
+ const stripped = command.replace(/<([^>]+)>/g, "$1");
903
+ const firstAngle = command.indexOf("<");
904
+ if (firstAngle >= 0) {
905
+ return {
906
+ lbuffer: stripped.slice(0, firstAngle),
907
+ rbuffer: stripped.slice(firstAngle) + comment
908
+ };
909
+ }
910
+ return { lbuffer: command, rbuffer: comment };
911
+ }
912
+ async function runForegroundClient(args) {
913
+ const { requestFile, resultFile, resultMode } = args;
914
+ const uid = typeof process.getuid === "function" ? process.getuid() : 0;
915
+ const socketPath2 = socketPathForUid(uid);
916
+ const inZellij = process.env.ZELLIJ !== void 0;
917
+ void appendDebugLog("client", "foreground start", {
918
+ requestFile,
919
+ resultFile,
920
+ resultMode,
921
+ socketPath: socketPath2,
922
+ inZellij
923
+ });
924
+ const ttyHandle = await fsp4.open("/dev/tty", "r+").catch(() => null);
925
+ try {
926
+ const raw = await fsp4.readFile(requestFile, "utf-8");
927
+ const request = shellRequestSchema.parse(JSON.parse(raw.trim()));
928
+ void appendDebugLog("client", "request parsed", request);
929
+ const detectedProvider = await detectProvider();
930
+ void appendDebugLog("client", "provider detected", { kind: detectedProvider.kind });
931
+ await ensureDaemon(socketPath2);
932
+ void appendDebugLog("client", "daemon ensured", { socketPath: socketPath2 });
933
+ if (resultMode === "replace-buffer-fixture") {
934
+ const fixtureLbuffer = request.lbuffer || "echo hello";
935
+ await writeShellResult(resultFile, {
936
+ kind: "replace-buffer",
937
+ lbuffer: fixtureLbuffer,
938
+ rbuffer: request.rbuffer
939
+ });
940
+ void appendDebugLog("client", "wrote replace-buffer result", {
941
+ resultFile,
942
+ lbuffer: fixtureLbuffer,
943
+ rbuffer: request.rbuffer
944
+ });
945
+ } else if (resultMode === "llm") {
946
+ try {
947
+ const decision = classifyIntent({ ...request, intent: "unknown", confidence: 0 });
948
+ void appendDebugLog("client", "intent classified", {
949
+ intent: decision.intent,
950
+ signals: decision.signals
951
+ });
952
+ const normalized = {
953
+ ...request,
954
+ intent: decision.intent,
955
+ confidence: decision.confidence
956
+ };
957
+ const envelope = await gatherContext(normalized);
958
+ void appendDebugLog("client", "context gathered", {
959
+ extraCount: envelope.extras.length
960
+ });
961
+ let ttyReadStream;
962
+ let ttyWriteStream;
963
+ if (ttyHandle) {
964
+ try {
965
+ ttyReadStream = new tty.ReadStream(ttyHandle.fd);
966
+ ttyWriteStream = new tty.WriteStream(ttyHandle.fd);
967
+ } catch {
968
+ }
969
+ }
970
+ const MODAL_VIEWPORT_LINES = 16;
971
+ if (ttyWriteStream && !inZellij) {
972
+ ttyWriteStream.write("\n".repeat(MODAL_VIEWPORT_LINES));
973
+ ttyWriteStream.write(`\x1B[${MODAL_VIEWPORT_LINES}A`);
974
+ ttyWriteStream.write("\x1B7");
975
+ }
976
+ await new Promise((resolve2) => {
977
+ let unmount;
978
+ let resolved = false;
979
+ const buildCandidateElement = (candidates, errorState) => React.createElement(CandidateSelect, {
980
+ candidates,
981
+ initialQuery: request.lbuffer,
982
+ error: errorState,
983
+ onSelect: async (command, explanation) => {
984
+ if (resolved) return;
985
+ resolved = true;
986
+ const { lbuffer, rbuffer } = buildShellBuffers(command, explanation);
987
+ await writeShellResult(resultFile, {
988
+ kind: "replace-buffer",
989
+ lbuffer,
990
+ rbuffer,
991
+ query: request.lbuffer
992
+ });
993
+ unmount?.();
994
+ },
995
+ onCancel: async () => {
996
+ if (resolved) return;
997
+ resolved = true;
998
+ await writeShellResult(resultFile, { kind: "cancel" });
999
+ unmount?.();
1000
+ }
1001
+ });
1002
+ const renderOptions = ttyReadStream && ttyWriteStream ? { stdin: ttyReadStream, stdout: ttyWriteStream, interactive: true } : { interactive: true };
1003
+ const app = render(buildCandidateElement(null), renderOptions);
1004
+ const clearScrollReserve = () => {
1005
+ if (ttyWriteStream && !inZellij) {
1006
+ try {
1007
+ ttyWriteStream.write("\x1B8\x1B[J");
1008
+ } catch {
1009
+ }
1010
+ }
1011
+ };
1012
+ const cleanupOnSignal = () => {
1013
+ app.unmount();
1014
+ clearScrollReserve();
1015
+ process.exit(0);
1016
+ };
1017
+ process.once("SIGHUP", cleanupOnSignal);
1018
+ process.once("SIGTERM", cleanupOnSignal);
1019
+ unmount = () => {
1020
+ process.off("SIGHUP", cleanupOnSignal);
1021
+ process.off("SIGTERM", cleanupOnSignal);
1022
+ app.unmount();
1023
+ clearScrollReserve();
1024
+ resolve2();
1025
+ };
1026
+ fetchCandidates(envelope, request.rbuffer).then((candidates) => {
1027
+ void appendDebugLog("client", "candidates received", { count: candidates.length });
1028
+ app.rerender(buildCandidateElement(candidates));
1029
+ }).catch(async (err) => {
1030
+ const message = err instanceof Error ? err.message : String(err);
1031
+ void appendDebugLog("client", "llm request failed", { message });
1032
+ if (resolved) return;
1033
+ resolved = true;
1034
+ try {
1035
+ await writeShellResult(resultFile, {
1036
+ kind: "error",
1037
+ message: `QueQue: ${message} \u2014 press any key`
1038
+ });
1039
+ } finally {
1040
+ unmount?.();
1041
+ }
1042
+ });
1043
+ });
1044
+ void appendDebugLog("client", "wrote llm result", { resultFile });
1045
+ } catch (error) {
1046
+ const message = error instanceof Error ? error.message : String(error);
1047
+ void appendDebugLog("client", "llm request failed", {
1048
+ message
1049
+ });
1050
+ await writeShellResult(resultFile, {
1051
+ kind: "error",
1052
+ message: `QueQue: ${message} \u2014 press any key`
1053
+ });
1054
+ }
1055
+ } else {
1056
+ await writeShellResult(resultFile, { kind: "cancel" });
1057
+ void appendDebugLog("client", "wrote cancel result", { resultFile });
1058
+ }
1059
+ } finally {
1060
+ await ttyHandle?.close();
1061
+ }
1062
+ }
1063
+
1064
+ // src/cli/commands/client.ts
1065
+ function parseResultMode(mode) {
1066
+ if (mode === void 0 || mode === "llm") return "llm";
1067
+ if (mode === "cancel") return "cancel";
1068
+ if (mode === "replace-buffer-fixture") return "replace-buffer-fixture";
1069
+ void appendDebugLog("client", `unknown --result-mode "${mode}", defaulting to cancel`);
1070
+ return "cancel";
1071
+ }
1072
+ async function clientCommand(options) {
1073
+ const resultMode = parseResultMode(options.resultMode);
1074
+ void appendDebugLog("client", "command start", {
1075
+ requestFile: options.requestFile,
1076
+ resultFile: options.resultFile,
1077
+ resultMode
1078
+ });
1079
+ await runForegroundClient({
1080
+ requestFile: options.requestFile,
1081
+ resultFile: options.resultFile,
1082
+ resultMode
1083
+ });
1084
+ }
1085
+
1086
+ // src/daemon/server.ts
1087
+ import * as net2 from "net";
1088
+
1089
+ // src/contracts/ipc.ts
1090
+ import { z as z4 } from "zod";
1091
+ var ipcRequestSchema = z4.discriminatedUnion("kind", [
1092
+ z4.object({
1093
+ kind: z4.literal("ping")
1094
+ }),
1095
+ z4.object({
1096
+ kind: z4.literal("ensure-session"),
1097
+ shellPid: z4.number().int().positive(),
1098
+ ttyPath: z4.string()
1099
+ }),
1100
+ z4.object({
1101
+ kind: z4.literal("run-query"),
1102
+ request: shellRequestSchema
1103
+ })
1104
+ ]);
1105
+ var ipcResponseSchema = z4.discriminatedUnion("kind", [
1106
+ z4.object({
1107
+ kind: z4.literal("pong")
1108
+ }),
1109
+ z4.object({
1110
+ kind: z4.literal("session-ready"),
1111
+ socketPath: z4.string()
1112
+ }),
1113
+ z4.object({
1114
+ kind: z4.literal("query-accepted"),
1115
+ requestId: z4.string()
1116
+ })
1117
+ ]);
1118
+
1119
+ // src/daemon/server.ts
1120
+ function startDaemonServer(socketPath2) {
1121
+ return new Promise((resolve2, reject) => {
1122
+ const MAX_BUF_BYTES = 64 * 1024;
1123
+ const server = net2.createServer((socket) => {
1124
+ let buf = "";
1125
+ void appendDebugLog("daemon", "socket connected", { socketPath: socketPath2 });
1126
+ socket.on("data", (chunk) => {
1127
+ if (buf.length + chunk.length > MAX_BUF_BYTES) {
1128
+ socket.destroy(new Error("IPC message too large"));
1129
+ return;
1130
+ }
1131
+ buf += chunk.toString();
1132
+ let nl = buf.indexOf("\n");
1133
+ while (nl !== -1) {
1134
+ const line = buf.slice(0, nl).trim();
1135
+ buf = buf.slice(nl + 1);
1136
+ nl = buf.indexOf("\n");
1137
+ if (!line) continue;
1138
+ let parsed;
1139
+ try {
1140
+ parsed = JSON.parse(line);
1141
+ } catch {
1142
+ continue;
1143
+ }
1144
+ const result = ipcRequestSchema.safeParse(parsed);
1145
+ if (!result.success) {
1146
+ void appendDebugLog("daemon", "ignored unknown request", { line });
1147
+ continue;
1148
+ }
1149
+ const req = result.data;
1150
+ void appendDebugLog("daemon", "request received", req);
1151
+ switch (req.kind) {
1152
+ case "ping": {
1153
+ socket.write(`${JSON.stringify({ kind: "pong" })}
1154
+ `);
1155
+ void appendDebugLog("daemon", "replied pong");
1156
+ break;
1157
+ }
1158
+ case "ensure-session": {
1159
+ socket.write(`${JSON.stringify({ kind: "session-ready", socketPath: socketPath2 })}
1160
+ `);
1161
+ void appendDebugLog("daemon", "replied session-ready", { socketPath: socketPath2 });
1162
+ break;
1163
+ }
1164
+ case "run-query": {
1165
+ const requestId = Math.random().toString(36).slice(2);
1166
+ socket.write(`${JSON.stringify({ kind: "query-accepted", requestId })}
1167
+ `);
1168
+ void appendDebugLog("daemon", "replied query-accepted", { requestId });
1169
+ break;
1170
+ }
1171
+ }
1172
+ }
1173
+ });
1174
+ socket.on("error", () => {
1175
+ });
1176
+ });
1177
+ server.on("error", reject);
1178
+ server.listen(socketPath2, () => {
1179
+ void appendDebugLog("daemon", "listening", { socketPath: socketPath2 });
1180
+ resolve2(server);
1181
+ });
1182
+ });
1183
+ }
1184
+
1185
+ // src/cli/commands/daemon.ts
1186
+ async function daemonCommand(options) {
1187
+ const sock = options.socket ?? socketPath();
1188
+ void appendDebugLog("daemon", "command start", { socket: sock, ensure: options.ensure });
1189
+ if (options.ensure) {
1190
+ await ensureDaemon(sock);
1191
+ void appendDebugLog("daemon", "ensure command complete", { socket: sock });
1192
+ return;
1193
+ }
1194
+ const server = await startDaemonServer(sock);
1195
+ void appendDebugLog("daemon", "server started", { socket: sock });
1196
+ const shutdown = () => {
1197
+ void appendDebugLog("daemon", "shutdown", { socket: sock });
1198
+ server.close(() => {
1199
+ process.exit(0);
1200
+ });
1201
+ };
1202
+ process.once("SIGTERM", shutdown);
1203
+ process.once("SIGINT", shutdown);
1204
+ await new Promise(() => {
1205
+ });
1206
+ }
1207
+
1208
+ // src/cli/main.ts
1209
+ function requiredOption(name, value) {
1210
+ if (!value) {
1211
+ throw new Error(`Missing required option: ${name}`);
1212
+ }
1213
+ return value;
1214
+ }
1215
+ var QQ_RESULT_FILE_PATTERN = /^\/tmp\/qq-sess\.[A-Za-z0-9]+\//;
1216
+ process.on("uncaughtException", (err) => {
1217
+ console.error("QueQue: uncaught exception:", err.message);
1218
+ const resultFile = process.env.QQ_RESULT_FILE;
1219
+ if (resultFile && QQ_RESULT_FILE_PATTERN.test(resultFile)) {
1220
+ try {
1221
+ fs2.writeFileSync(resultFile, '{"kind":"cancel"}\n');
1222
+ } catch {
1223
+ }
1224
+ }
1225
+ process.exit(1);
1226
+ });
1227
+ process.on("unhandledRejection", (reason) => {
1228
+ const message = reason instanceof Error ? reason.message : String(reason);
1229
+ console.error("QueQue: unhandled rejection:", message);
1230
+ const resultFile = process.env.QQ_RESULT_FILE;
1231
+ if (resultFile && QQ_RESULT_FILE_PATTERN.test(resultFile)) {
1232
+ try {
1233
+ fs2.writeFileSync(resultFile, '{"kind":"cancel"}\n');
1234
+ } catch {
1235
+ }
1236
+ }
1237
+ process.exit(1);
1238
+ });
1239
+ async function main(argv = process.argv.slice(2)) {
1240
+ bootstrapBuiltins();
1241
+ const cli = cac("qq");
1242
+ cli.command("client").option("--request-file <path>", "Path to the serialized shell request").option("--result-file <path>", "Path to write the serialized shell result").option("--result-mode <mode>", "Result mode: llm | cancel | replace-buffer-fixture").action(async (options) => {
1243
+ const requestFile = requiredOption("--request-file", options.requestFile);
1244
+ const resultFile = requiredOption("--result-file", options.resultFile);
1245
+ await clientCommand({ requestFile, resultFile, resultMode: options.resultMode });
1246
+ });
1247
+ cli.command("daemon").option("--socket <path>", "Unix socket path for the daemon").option("--ensure", "Ensure the daemon is running before returning").action(async (options) => {
1248
+ await daemonCommand(options);
1249
+ });
1250
+ cli.help();
1251
+ cli.parse(["node", "qq", ...argv], { run: false });
1252
+ await cli.runMatchedCommand();
1253
+ }
1254
+ var isDirectRun = process.argv[1] ? import.meta.url === new URL(`file://${process.argv[1]}`).href : false;
1255
+ if (isDirectRun) {
1256
+ void main().catch((error) => {
1257
+ const message = error instanceof Error ? error.message : String(error);
1258
+ console.error(message);
1259
+ process.exitCode = 1;
1260
+ });
1261
+ }
1262
+ export {
1263
+ main
1264
+ };
1265
+ //# sourceMappingURL=main.js.map