@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/README.md +38 -0
- package/dist/cli/main.js +699 -286
- package/dist/cli/main.js.map +1 -1
- package/package.json +2 -6
- package/shell/zsh/queque.zsh +8 -1
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(
|
|
177
|
-
if (registry2.has(
|
|
178
|
-
throw new Error(`Provider backend already registered: "${
|
|
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(
|
|
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
|
|
585
|
+
import { z as z4 } from "zod";
|
|
238
586
|
|
|
239
587
|
// src/contracts/shell.ts
|
|
240
|
-
import { z } from "zod";
|
|
241
|
-
var shellRequestSchema =
|
|
242
|
-
version:
|
|
243
|
-
ttyPath:
|
|
244
|
-
cwd:
|
|
245
|
-
shellPid:
|
|
246
|
-
lbuffer:
|
|
247
|
-
rbuffer:
|
|
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 =
|
|
250
|
-
|
|
251
|
-
kind:
|
|
597
|
+
var shellResultSchema = z3.discriminatedUnion("kind", [
|
|
598
|
+
z3.object({
|
|
599
|
+
kind: z3.literal("cancel")
|
|
252
600
|
}).strict(),
|
|
253
|
-
|
|
254
|
-
kind:
|
|
255
|
-
lbuffer:
|
|
256
|
-
rbuffer:
|
|
257
|
-
query:
|
|
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
|
-
|
|
260
|
-
kind:
|
|
261
|
-
message:
|
|
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 =
|
|
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:
|
|
623
|
+
confidence: z4.number().min(0).max(1)
|
|
276
624
|
});
|
|
277
|
-
var baseContextSchema =
|
|
278
|
-
queryText:
|
|
279
|
-
cwd:
|
|
280
|
-
ttyPath:
|
|
281
|
-
shellPid:
|
|
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:
|
|
284
|
-
platform:
|
|
285
|
-
timestamp:
|
|
631
|
+
shellName: z4.literal("zsh"),
|
|
632
|
+
platform: z4.string(),
|
|
633
|
+
timestamp: z4.string()
|
|
286
634
|
});
|
|
287
|
-
var intentDecisionSchema =
|
|
635
|
+
var intentDecisionSchema = z4.object({
|
|
288
636
|
intent: requestIntentSchema,
|
|
289
|
-
confidence:
|
|
290
|
-
signals:
|
|
637
|
+
confidence: z4.number().min(0).max(1),
|
|
638
|
+
signals: z4.array(z4.string())
|
|
291
639
|
});
|
|
292
|
-
var contextChunkSchema =
|
|
293
|
-
|
|
294
|
-
kind:
|
|
295
|
-
payload:
|
|
296
|
-
cwd:
|
|
297
|
-
root:
|
|
298
|
-
branch:
|
|
299
|
-
dirty:
|
|
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:
|
|
649
|
+
changedFiles: z4.array(z4.string())
|
|
302
650
|
})
|
|
303
651
|
}),
|
|
304
|
-
|
|
305
|
-
kind:
|
|
306
|
-
payload:
|
|
307
|
-
cwd:
|
|
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:
|
|
657
|
+
apparentFilename: z4.string().nullable()
|
|
310
658
|
})
|
|
311
659
|
})
|
|
312
660
|
]);
|
|
313
|
-
var contextEnvelopeSchema =
|
|
661
|
+
var contextEnvelopeSchema = z4.object({
|
|
314
662
|
base: baseContextSchema,
|
|
315
|
-
extras:
|
|
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
|
-
|
|
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
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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
|
|
840
|
-
|
|
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
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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.
|
|
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",
|
|
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
|
-
|
|
971
|
-
|
|
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
|
|
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
|
|
1133
|
-
var ipcRequestSchema =
|
|
1134
|
-
|
|
1135
|
-
kind:
|
|
1538
|
+
import { z as z5 } from "zod";
|
|
1539
|
+
var ipcRequestSchema = z5.discriminatedUnion("kind", [
|
|
1540
|
+
z5.object({
|
|
1541
|
+
kind: z5.literal("ping")
|
|
1136
1542
|
}),
|
|
1137
|
-
|
|
1138
|
-
kind:
|
|
1139
|
-
shellPid:
|
|
1140
|
-
ttyPath:
|
|
1543
|
+
z5.object({
|
|
1544
|
+
kind: z5.literal("ensure-session"),
|
|
1545
|
+
shellPid: z5.number().int().positive(),
|
|
1546
|
+
ttyPath: z5.string()
|
|
1141
1547
|
}),
|
|
1142
|
-
|
|
1143
|
-
kind:
|
|
1548
|
+
z5.object({
|
|
1549
|
+
kind: z5.literal("run-query"),
|
|
1144
1550
|
request: shellRequestSchema
|
|
1145
1551
|
})
|
|
1146
1552
|
]);
|
|
1147
|
-
var ipcResponseSchema =
|
|
1148
|
-
|
|
1149
|
-
kind:
|
|
1553
|
+
var ipcResponseSchema = z5.discriminatedUnion("kind", [
|
|
1554
|
+
z5.object({
|
|
1555
|
+
kind: z5.literal("pong")
|
|
1150
1556
|
}),
|
|
1151
|
-
|
|
1152
|
-
kind:
|
|
1153
|
-
socketPath:
|
|
1557
|
+
z5.object({
|
|
1558
|
+
kind: z5.literal("session-ready"),
|
|
1559
|
+
socketPath: z5.string()
|
|
1154
1560
|
}),
|
|
1155
|
-
|
|
1156
|
-
kind:
|
|
1157
|
-
requestId:
|
|
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
|
|
1252
|
-
import { homedir as
|
|
1253
|
-
import { dirname as dirname3, join as
|
|
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
|
-
|
|
1267
|
-
|
|
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 =
|
|
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) ?
|
|
1275
|
-
if (existing
|
|
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
|
}
|