@k-leumas/queque-cli 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/app.d.ts +3 -0
- package/dist/cli/app.js +60269 -0
- package/dist/cli/app.js.map +1 -0
- package/dist/cli/main.d.ts +1 -2
- package/dist/cli/main.js +31 -1745
- package/dist/cli/main.js.map +1 -1
- package/package.json +3 -2
- package/shell/zsh/queque.zsh +214 -35
package/dist/cli/main.js
CHANGED
|
@@ -1,1761 +1,47 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __quequeCreateRequire } from 'node:module';
|
|
3
|
+
const require = __quequeCreateRequire(import.meta.url);
|
|
2
4
|
|
|
3
|
-
// src/cli/
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
// src/context/providers/filesystem-context.ts
|
|
8
|
-
var FILENAME_RE = /\b[\w.-]+\.\w{2,6}\b/g;
|
|
9
|
-
var filesystemContextProvider = {
|
|
10
|
-
id: "filesystem-context",
|
|
11
|
-
intents: ["filesystem"],
|
|
12
|
-
async gather(input) {
|
|
13
|
-
const match = input.base.queryText.match(FILENAME_RE);
|
|
14
|
-
return {
|
|
15
|
-
kind: "filesystem",
|
|
16
|
-
payload: {
|
|
17
|
-
cwd: input.base.cwd,
|
|
18
|
-
apparentFilename: match?.[0] ?? null
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
// src/context/providers/git-context.ts
|
|
25
|
-
import { execFile as execFile2 } from "child_process";
|
|
26
|
-
import { statSync } from "fs";
|
|
27
|
-
import { promisify as promisify2 } from "util";
|
|
28
|
-
|
|
29
|
-
// src/shared/debug-log.ts
|
|
30
|
-
import * as fsp from "fs/promises";
|
|
31
|
-
|
|
32
|
-
// src/shared/qq-config.ts
|
|
33
|
-
import { readFileSync } from "fs";
|
|
5
|
+
// src/cli/launcher.ts
|
|
6
|
+
import { lstatSync, mkdirSync } from "fs";
|
|
7
|
+
import * as nodeModule from "module";
|
|
34
8
|
import { homedir } from "os";
|
|
35
|
-
import { join } from "path";
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
190
|
-
var debugLogPath = process.env.QQ_DEBUG_LOG_FILE ?? `/tmp/qq-${process.getuid?.() ?? "unknown"}-debug.log`;
|
|
191
|
-
function formatDetails(details) {
|
|
192
|
-
if (details === void 0) return "";
|
|
193
|
-
try {
|
|
194
|
-
return ` ${JSON.stringify(details)}`;
|
|
195
|
-
} catch {
|
|
196
|
-
return " [unserializable-details]";
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
async function appendDebugLog(scope, message, details) {
|
|
200
|
-
const line = `${(/* @__PURE__ */ new Date()).toISOString()} [${scope}] ${message}${formatDetails(redactForLog(details))}
|
|
201
|
-
`;
|
|
202
|
-
try {
|
|
203
|
-
await fsp.appendFile(debugLogPath, line, { encoding: "utf-8", mode: 384 });
|
|
204
|
-
} catch {
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// src/shared/vcs-context.ts
|
|
209
|
-
import { execFile } from "child_process";
|
|
210
|
-
import { promisify } from "util";
|
|
211
|
-
var execFileAsync = promisify(execFile);
|
|
212
|
-
async function runGit(cwd, args) {
|
|
213
|
-
try {
|
|
214
|
-
const { stdout } = await execFileAsync("git", ["-C", cwd, ...args], { encoding: "utf8" });
|
|
215
|
-
return stdout.trim();
|
|
216
|
-
} catch {
|
|
217
|
-
return null;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
async function detectVcsContext(cwd) {
|
|
221
|
-
const insideWorkTree = await runGit(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
222
|
-
if (insideWorkTree !== "true") {
|
|
223
|
-
return { kind: "none", cwd };
|
|
224
|
-
}
|
|
225
|
-
const root = await runGit(cwd, ["rev-parse", "--show-toplevel"]) ?? cwd;
|
|
226
|
-
const branch = await runGit(cwd, ["branch", "--show-current"]) || null;
|
|
227
|
-
const dirty = Boolean((await runGit(cwd, ["status", "--porcelain=v1"]))?.length);
|
|
228
|
-
return {
|
|
229
|
-
kind: "git",
|
|
230
|
-
cwd,
|
|
231
|
-
root,
|
|
232
|
-
branch,
|
|
233
|
-
dirty
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// src/context/providers/git-context.ts
|
|
238
|
-
var execFileAsync2 = promisify2(execFile2);
|
|
239
|
-
var GIT_TIMEOUT_MS = 5e3;
|
|
240
|
-
function assertSafeCwd(cwd) {
|
|
241
|
-
let st;
|
|
242
|
-
try {
|
|
243
|
-
st = statSync(cwd);
|
|
244
|
-
} catch {
|
|
245
|
-
throw new Error(`cwd not accessible: ${cwd}`);
|
|
246
|
-
}
|
|
247
|
-
if (!st.isDirectory()) throw new Error(`cwd is not a directory: ${cwd}`);
|
|
248
|
-
}
|
|
249
|
-
function unescapeGitPath(s) {
|
|
250
|
-
return s.replace(/\\([0-7]{3})/g, (_, oct) => String.fromCodePoint(parseInt(oct, 8))).replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\\\/g, "\\");
|
|
251
|
-
}
|
|
252
|
-
function stripQuotes(value) {
|
|
253
|
-
if (value.startsWith('"') && value.endsWith('"')) {
|
|
254
|
-
return unescapeGitPath(value.slice(1, -1));
|
|
255
|
-
}
|
|
256
|
-
return value;
|
|
257
|
-
}
|
|
258
|
-
function parsePorcelainLine(line) {
|
|
259
|
-
if (line.length < 4) {
|
|
260
|
-
return null;
|
|
261
|
-
}
|
|
262
|
-
const rest = line.slice(3);
|
|
263
|
-
const renameSeparator = " -> ";
|
|
264
|
-
const renameIndex = rest.indexOf(renameSeparator);
|
|
265
|
-
if (renameIndex !== -1) {
|
|
266
|
-
return stripQuotes(rest.slice(renameIndex + renameSeparator.length));
|
|
267
|
-
}
|
|
268
|
-
return stripQuotes(rest);
|
|
269
|
-
}
|
|
270
|
-
async function getChangedFiles(cwd) {
|
|
271
|
-
try {
|
|
272
|
-
assertSafeCwd(cwd);
|
|
273
|
-
const { stdout } = await execFileAsync2("git", ["-C", cwd, "status", "--porcelain"], {
|
|
274
|
-
encoding: "utf8",
|
|
275
|
-
timeout: GIT_TIMEOUT_MS
|
|
276
|
-
});
|
|
277
|
-
return stdout.split("\n").filter(Boolean).map(parsePorcelainLine).filter((path2) => path2 !== null);
|
|
278
|
-
} catch {
|
|
279
|
-
return [];
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
var gitContextProvider = {
|
|
283
|
-
id: "git-context",
|
|
284
|
-
intents: ["codebase", "shell-command"],
|
|
285
|
-
async gather(input) {
|
|
286
|
-
if (input.decision.intent === "shell-command" && !input.decision.signals.includes("git-prefix")) {
|
|
287
|
-
return null;
|
|
288
|
-
}
|
|
289
|
-
try {
|
|
290
|
-
const vcs = await detectVcsContext(input.base.cwd);
|
|
291
|
-
if (vcs.kind === "none") {
|
|
292
|
-
return null;
|
|
293
|
-
}
|
|
294
|
-
const changedFiles = await getChangedFiles(input.base.cwd);
|
|
295
|
-
void appendDebugLog("context", "git provider gathered", {
|
|
296
|
-
cwd: input.base.cwd,
|
|
297
|
-
branch: vcs.branch,
|
|
298
|
-
dirty: vcs.dirty,
|
|
299
|
-
changedFiles: changedFiles.length
|
|
300
|
-
});
|
|
301
|
-
return {
|
|
302
|
-
kind: "git",
|
|
303
|
-
payload: {
|
|
304
|
-
cwd: vcs.cwd,
|
|
305
|
-
root: vcs.root,
|
|
306
|
-
branch: vcs.branch,
|
|
307
|
-
dirty: vcs.dirty,
|
|
308
|
-
changedFiles
|
|
309
|
-
}
|
|
310
|
-
};
|
|
311
|
-
} catch (error) {
|
|
312
|
-
void appendDebugLog("context", "git provider failed", {
|
|
313
|
-
cwd: input.base.cwd,
|
|
314
|
-
message: error instanceof Error ? error.message : String(error)
|
|
315
|
-
});
|
|
316
|
-
return null;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
};
|
|
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-sonnet-5";
|
|
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();
|
|
9
|
+
import { isAbsolute, join } from "path";
|
|
10
|
+
var SHARED_WRITE_BITS = 18;
|
|
11
|
+
function isPrivateDirectory(path, uid) {
|
|
12
|
+
const stats = lstatSync(path);
|
|
13
|
+
return stats.isDirectory() && !stats.isSymbolicLink() && stats.uid === uid && (stats.mode & SHARED_WRITE_BITS) === 0;
|
|
393
14
|
}
|
|
394
|
-
function
|
|
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
|
-
});
|
|
15
|
+
function enablePerUserCompileCache() {
|
|
469
16
|
try {
|
|
470
|
-
|
|
471
|
-
{
|
|
472
|
-
model,
|
|
473
|
-
max_tokens: 1024,
|
|
474
|
-
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.",
|
|
475
|
-
messages: [{ role: "user", content: prompt }]
|
|
476
|
-
},
|
|
477
|
-
{
|
|
478
|
-
timeout: 25e3
|
|
479
|
-
// 25s — slightly under the zsh 30s FIFO timeout
|
|
480
|
-
}
|
|
481
|
-
);
|
|
482
|
-
const text = extractText(response.content);
|
|
483
|
-
const candidates = ensureSelectableCandidates(parseCandidates(text));
|
|
484
|
-
void appendDebugLog("provider", "response parsed", {
|
|
485
|
-
model,
|
|
486
|
-
candidateCount: candidates.length,
|
|
487
|
-
forceSelector: shouldForceSelector()
|
|
488
|
-
});
|
|
489
|
-
return candidates;
|
|
490
|
-
} catch (error) {
|
|
491
|
-
void appendDebugLog("provider", "request failed", {
|
|
492
|
-
model,
|
|
493
|
-
message: error instanceof Error ? error.message : String(error)
|
|
494
|
-
});
|
|
495
|
-
throw error;
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
var claudeAdapter = {
|
|
499
|
-
fetchCandidates
|
|
500
|
-
};
|
|
501
|
-
|
|
502
|
-
// src/registry/context-providers.ts
|
|
503
|
-
var registry = /* @__PURE__ */ new Map();
|
|
504
|
-
function registerContextProvider(descriptor) {
|
|
505
|
-
if (registry.has(descriptor.id)) {
|
|
506
|
-
throw new Error(`Context provider already registered: "${descriptor.id}"`);
|
|
507
|
-
}
|
|
508
|
-
registry.set(descriptor.id, descriptor);
|
|
509
|
-
}
|
|
510
|
-
function listContextProviders() {
|
|
511
|
-
return Array.from(registry.values());
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
// src/registry/provider-backends.ts
|
|
515
|
-
var registry2 = /* @__PURE__ */ new Map();
|
|
516
|
-
function registerProviderBackend(registration) {
|
|
517
|
-
if (registry2.has(registration.id)) {
|
|
518
|
-
throw new Error(`Provider backend already registered: "${registration.id}"`);
|
|
519
|
-
}
|
|
520
|
-
registry2.set(registration.id, registration);
|
|
521
|
-
}
|
|
522
|
-
function getProviderAdapter(id) {
|
|
523
|
-
return registry2.get(id)?.adapter;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// src/registry/shell-adapters.ts
|
|
527
|
-
var registry3 = /* @__PURE__ */ new Map();
|
|
528
|
-
function registerShellAdapter(descriptor) {
|
|
529
|
-
if (registry3.has(descriptor.id)) {
|
|
530
|
-
throw new Error(`Shell adapter already registered: "${descriptor.id}"`);
|
|
531
|
-
}
|
|
532
|
-
registry3.set(descriptor.id, descriptor);
|
|
533
|
-
}
|
|
534
|
-
function listShellAdapters() {
|
|
535
|
-
return Array.from(registry3.values());
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// src/registry/storage-hooks.ts
|
|
539
|
-
var registry4 = /* @__PURE__ */ new Map();
|
|
540
|
-
function registerStorageHook(descriptor) {
|
|
541
|
-
if (registry4.has(descriptor.id)) {
|
|
542
|
-
throw new Error(`Storage hook already registered: "${descriptor.id}"`);
|
|
543
|
-
}
|
|
544
|
-
registry4.set(descriptor.id, descriptor);
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// src/registry/bootstrap.ts
|
|
548
|
-
var bootstrapped = false;
|
|
549
|
-
function bootstrapBuiltins() {
|
|
550
|
-
if (bootstrapped) {
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
bootstrapped = true;
|
|
554
|
-
registerContextProvider(gitContextProvider);
|
|
555
|
-
registerContextProvider(filesystemContextProvider);
|
|
556
|
-
registerShellAdapter({
|
|
557
|
-
id: "zsh",
|
|
558
|
-
shell: "zsh",
|
|
559
|
-
description: "zsh ZLE widget adapter (Phase 2 built-in)"
|
|
560
|
-
});
|
|
561
|
-
registerStorageHook({
|
|
562
|
-
id: "noop",
|
|
563
|
-
description: "No-op storage hook (default \u2014 no persistence)"
|
|
564
|
-
});
|
|
565
|
-
registerStorageHook({
|
|
566
|
-
id: "memory",
|
|
567
|
-
description: "In-memory storage hook (test and ephemeral use)"
|
|
568
|
-
});
|
|
569
|
-
registerProviderBackend({
|
|
570
|
-
id: "claude",
|
|
571
|
-
name: "Claude (Anthropic)",
|
|
572
|
-
description: "Anthropic Claude adapter \u2014 default LLM backend",
|
|
573
|
-
adapter: claudeAdapter
|
|
574
|
-
});
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// src/client/run-foreground.ts
|
|
578
|
-
import * as fsp4 from "fs/promises";
|
|
579
|
-
import * as tty from "tty";
|
|
580
|
-
import { render } from "ink";
|
|
581
|
-
import React from "react";
|
|
582
|
-
|
|
583
|
-
// src/contracts/request.ts
|
|
584
|
-
import { z as z4 } from "zod";
|
|
585
|
-
|
|
586
|
-
// src/contracts/shell.ts
|
|
587
|
-
import { z as z3 } from "zod";
|
|
588
|
-
var shellRequestSchema = z3.object({
|
|
589
|
-
version: z3.literal(1),
|
|
590
|
-
ttyPath: z3.string(),
|
|
591
|
-
cwd: z3.string().regex(/^\/[^\0]*$/, "cwd must be an absolute POSIX path"),
|
|
592
|
-
shellPid: z3.number().int().positive(),
|
|
593
|
-
lbuffer: z3.string(),
|
|
594
|
-
rbuffer: z3.string()
|
|
595
|
-
});
|
|
596
|
-
var shellResultSchema = z3.discriminatedUnion("kind", [
|
|
597
|
-
z3.object({
|
|
598
|
-
kind: z3.literal("cancel")
|
|
599
|
-
}).strict(),
|
|
600
|
-
z3.object({
|
|
601
|
-
kind: z3.literal("replace-buffer"),
|
|
602
|
-
lbuffer: z3.string(),
|
|
603
|
-
rbuffer: z3.string(),
|
|
604
|
-
query: z3.string().optional()
|
|
605
|
-
}),
|
|
606
|
-
z3.object({
|
|
607
|
-
kind: z3.literal("error"),
|
|
608
|
-
message: z3.string()
|
|
609
|
-
})
|
|
610
|
-
]);
|
|
611
|
-
|
|
612
|
-
// src/contracts/request.ts
|
|
613
|
-
var requestIntentSchema = z4.enum([
|
|
614
|
-
"shell-command",
|
|
615
|
-
"codebase",
|
|
616
|
-
"filesystem",
|
|
617
|
-
"general",
|
|
618
|
-
"unknown"
|
|
619
|
-
]);
|
|
620
|
-
var normalizedRequestSchema = shellRequestSchema.extend({
|
|
621
|
-
intent: requestIntentSchema,
|
|
622
|
-
confidence: z4.number().min(0).max(1)
|
|
623
|
-
});
|
|
624
|
-
var baseContextSchema = z4.object({
|
|
625
|
-
queryText: z4.string(),
|
|
626
|
-
cwd: z4.string(),
|
|
627
|
-
ttyPath: z4.string(),
|
|
628
|
-
shellPid: z4.number().int().positive(),
|
|
629
|
-
/** Hardcoded 'zsh' for Phase 2 — cross-shell support is v2 scope. */
|
|
630
|
-
shellName: z4.literal("zsh"),
|
|
631
|
-
platform: z4.string(),
|
|
632
|
-
timestamp: z4.string()
|
|
633
|
-
});
|
|
634
|
-
var intentDecisionSchema = z4.object({
|
|
635
|
-
intent: requestIntentSchema,
|
|
636
|
-
confidence: z4.number().min(0).max(1),
|
|
637
|
-
signals: z4.array(z4.string())
|
|
638
|
-
});
|
|
639
|
-
var contextChunkSchema = z4.discriminatedUnion("kind", [
|
|
640
|
-
z4.object({
|
|
641
|
-
kind: z4.literal("git"),
|
|
642
|
-
payload: z4.object({
|
|
643
|
-
cwd: z4.string(),
|
|
644
|
-
root: z4.string(),
|
|
645
|
-
branch: z4.string().nullable(),
|
|
646
|
-
dirty: z4.boolean(),
|
|
647
|
-
/** File paths only (from git status --porcelain). No diff content, no file bytes. */
|
|
648
|
-
changedFiles: z4.array(z4.string())
|
|
649
|
-
})
|
|
650
|
-
}),
|
|
651
|
-
z4.object({
|
|
652
|
-
kind: z4.literal("filesystem"),
|
|
653
|
-
payload: z4.object({
|
|
654
|
-
cwd: z4.string(),
|
|
655
|
-
/** Apparent filename extracted from query text. No file bytes read. */
|
|
656
|
-
apparentFilename: z4.string().nullable()
|
|
657
|
-
})
|
|
658
|
-
})
|
|
659
|
-
]);
|
|
660
|
-
var contextEnvelopeSchema = z4.object({
|
|
661
|
-
base: baseContextSchema,
|
|
662
|
-
extras: z4.array(contextChunkSchema)
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
// src/intent/router.ts
|
|
666
|
-
var GIT_PREFIX_RE = /^git\s/;
|
|
667
|
-
var PKG_MANAGER_RE = /^(npm|pnpm|yarn)\s+(test|build|lint|run|start|dev|check|format|typecheck)\b/i;
|
|
668
|
-
var FILESYSTEM_KEYWORDS_RE = /\b(?:rename|move(?:\s+file)?|find\s+files?|copy|delete|list\s+files?|image|folder|directory)\b/i;
|
|
669
|
-
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|\|/;
|
|
670
|
-
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|$))/;
|
|
671
|
-
function classifyIntent(request) {
|
|
672
|
-
const query = request.lbuffer.trim();
|
|
673
|
-
if (query.length === 0) {
|
|
674
|
-
return {
|
|
675
|
-
intent: requestIntentSchema.enum.unknown,
|
|
676
|
-
confidence: 1,
|
|
677
|
-
signals: ["empty-query"]
|
|
678
|
-
};
|
|
679
|
-
}
|
|
680
|
-
if (GIT_PREFIX_RE.test(query)) {
|
|
681
|
-
return {
|
|
682
|
-
intent: requestIntentSchema.enum["shell-command"],
|
|
683
|
-
confidence: 1,
|
|
684
|
-
signals: ["git-prefix"]
|
|
685
|
-
};
|
|
686
|
-
}
|
|
687
|
-
if (PKG_MANAGER_RE.test(query)) {
|
|
688
|
-
return {
|
|
689
|
-
intent: requestIntentSchema.enum.codebase,
|
|
690
|
-
confidence: 1,
|
|
691
|
-
signals: ["pkg-manager-script"]
|
|
692
|
-
};
|
|
693
|
-
}
|
|
694
|
-
if (FILESYSTEM_KEYWORDS_RE.test(query)) {
|
|
695
|
-
return {
|
|
696
|
-
intent: requestIntentSchema.enum.filesystem,
|
|
697
|
-
confidence: 0.85,
|
|
698
|
-
signals: ["filesystem-keyword"]
|
|
699
|
-
};
|
|
700
|
-
}
|
|
701
|
-
if (SHELL_COMMAND_RE.test(query)) {
|
|
702
|
-
return {
|
|
703
|
-
intent: requestIntentSchema.enum["shell-command"],
|
|
704
|
-
confidence: 0.8,
|
|
705
|
-
signals: ["shell-command-signal"]
|
|
706
|
-
};
|
|
707
|
-
}
|
|
708
|
-
if (FILE_PATH_RE.test(query)) {
|
|
709
|
-
return {
|
|
710
|
-
intent: requestIntentSchema.enum.codebase,
|
|
711
|
-
confidence: 0.9,
|
|
712
|
-
signals: ["file-path-signal"]
|
|
713
|
-
};
|
|
714
|
-
}
|
|
715
|
-
return {
|
|
716
|
-
intent: requestIntentSchema.enum.general,
|
|
717
|
-
confidence: 0.5,
|
|
718
|
-
signals: ["no-strong-signal"]
|
|
719
|
-
};
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
// src/context/base-context.ts
|
|
723
|
-
function buildBaseContext(request) {
|
|
724
|
-
return {
|
|
725
|
-
queryText: request.lbuffer.trim(),
|
|
726
|
-
cwd: request.cwd,
|
|
727
|
-
ttyPath: request.ttyPath,
|
|
728
|
-
shellPid: request.shellPid,
|
|
729
|
-
shellName: "zsh",
|
|
730
|
-
platform: process.platform,
|
|
731
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
732
|
-
};
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
// src/context/pipeline.ts
|
|
736
|
-
async function gatherContext(request) {
|
|
737
|
-
void appendDebugLog("context", "pipeline start", {
|
|
738
|
-
queryLength: request.lbuffer.length,
|
|
739
|
-
intent: request.intent
|
|
740
|
-
});
|
|
741
|
-
const base = buildBaseContext(request);
|
|
742
|
-
const decision = classifyIntent(request);
|
|
743
|
-
void appendDebugLog("context", "intent decision", {
|
|
744
|
-
intent: decision.intent,
|
|
745
|
-
signals: decision.signals
|
|
746
|
-
});
|
|
747
|
-
const extras = [];
|
|
748
|
-
const providers = listContextProviders();
|
|
749
|
-
for (const provider of providers) {
|
|
750
|
-
const supportsAllIntents = provider.intents.includes("*");
|
|
751
|
-
const supportsIntent = provider.intents.includes(decision.intent);
|
|
752
|
-
if (!supportsAllIntents && !supportsIntent) {
|
|
753
|
-
continue;
|
|
754
|
-
}
|
|
755
|
-
try {
|
|
756
|
-
const chunk = await provider.gather({ base, decision });
|
|
757
|
-
if (chunk !== null) {
|
|
758
|
-
extras.push(chunk);
|
|
759
|
-
void appendDebugLog("context", "provider chunk gathered", {
|
|
760
|
-
providerId: provider.id,
|
|
761
|
-
kind: chunk.kind
|
|
762
|
-
});
|
|
763
|
-
}
|
|
764
|
-
} catch (error) {
|
|
765
|
-
void appendDebugLog("context", "provider error", {
|
|
766
|
-
providerId: provider.id,
|
|
767
|
-
message: error instanceof Error ? error.message : String(error)
|
|
768
|
-
});
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
const envelope = filterContextEnvelope({ base, extras });
|
|
772
|
-
void appendDebugLog("context", "envelope assembled", { extraCount: extras.length });
|
|
773
|
-
return envelope;
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
// src/daemon/bootstrap.ts
|
|
777
|
-
import { spawn } from "child_process";
|
|
778
|
-
import * as fs from "fs";
|
|
779
|
-
import * as net from "net";
|
|
780
|
-
import * as path from "path";
|
|
781
|
-
import { fileURLToPath } from "url";
|
|
782
|
-
var CONNECT_TIMEOUT_MS = 500;
|
|
783
|
-
var POLL_INTERVAL_MS = 50;
|
|
784
|
-
var POLL_MAX_ATTEMPTS = 40;
|
|
785
|
-
function tryConnect(socketPath2) {
|
|
786
|
-
return new Promise((resolve3) => {
|
|
787
|
-
const timer = setTimeout(() => {
|
|
788
|
-
client.destroy();
|
|
789
|
-
resolve3(false);
|
|
790
|
-
}, CONNECT_TIMEOUT_MS);
|
|
791
|
-
const client = net.createConnection(socketPath2, () => {
|
|
792
|
-
clearTimeout(timer);
|
|
793
|
-
client.destroy();
|
|
794
|
-
resolve3(true);
|
|
795
|
-
});
|
|
796
|
-
client.on("error", () => {
|
|
797
|
-
clearTimeout(timer);
|
|
798
|
-
resolve3(false);
|
|
799
|
-
});
|
|
800
|
-
});
|
|
801
|
-
}
|
|
802
|
-
async function waitForSocket(socketPath2, maxAttempts = POLL_MAX_ATTEMPTS) {
|
|
803
|
-
for (let i = 0; i < maxAttempts; i++) {
|
|
804
|
-
if (await tryConnect(socketPath2)) return true;
|
|
805
|
-
await new Promise((res) => setTimeout(res, POLL_INTERVAL_MS));
|
|
806
|
-
}
|
|
807
|
-
return false;
|
|
808
|
-
}
|
|
809
|
-
function assertSafeSocketPath(socketPath2) {
|
|
810
|
-
const resolved = path.resolve(socketPath2);
|
|
811
|
-
const base = path.basename(resolved);
|
|
812
|
-
const dir = path.dirname(resolved);
|
|
813
|
-
const tmpRoots = ["/tmp", "/private/tmp"];
|
|
814
|
-
if (!tmpRoots.includes(dir) || !base.startsWith("qq-") || !base.endsWith(".sock")) {
|
|
815
|
-
throw new Error(`unsafe socket path rejected: ${socketPath2}`);
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
async function ensureDaemon(socketPath2) {
|
|
819
|
-
assertSafeSocketPath(socketPath2);
|
|
820
|
-
void appendDebugLog("daemon", "ensure start", { socketPath: socketPath2 });
|
|
821
|
-
if (await tryConnect(socketPath2)) {
|
|
822
|
-
void appendDebugLog("daemon", "already running", { socketPath: socketPath2 });
|
|
823
|
-
return;
|
|
824
|
-
}
|
|
825
|
-
try {
|
|
826
|
-
fs.unlinkSync(socketPath2);
|
|
827
|
-
} catch {
|
|
828
|
-
}
|
|
829
|
-
const qqBin = process.execPath;
|
|
830
|
-
const qqScript = fileURLToPath(import.meta.url);
|
|
831
|
-
if (!fs.existsSync(qqScript)) {
|
|
832
|
-
void appendDebugLog("daemon", "script missing", { qqScript });
|
|
833
|
-
throw new Error(`qq daemon script not found at: ${qqScript}. Run 'pnpm build' first.`);
|
|
834
|
-
}
|
|
835
|
-
void appendDebugLog("daemon", "spawning daemon", { qqScript, socketPath: socketPath2 });
|
|
836
|
-
const child = spawn(qqBin, [qqScript, "daemon", "--socket", socketPath2], {
|
|
837
|
-
detached: true,
|
|
838
|
-
stdio: "ignore"
|
|
839
|
-
});
|
|
840
|
-
child.unref();
|
|
841
|
-
const started = await waitForSocket(socketPath2);
|
|
842
|
-
if (!started) {
|
|
843
|
-
void appendDebugLog("daemon", "startup timeout", { socketPath: socketPath2 });
|
|
844
|
-
throw new Error(`qq daemon did not start within the expected window (${socketPath2})`);
|
|
845
|
-
}
|
|
846
|
-
void appendDebugLog("daemon", "daemon ready", { socketPath: socketPath2 });
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
// src/providers/detect.ts
|
|
850
|
-
import { execSync } from "child_process";
|
|
851
|
-
import * as fsp2 from "fs/promises";
|
|
852
|
-
import * as os from "os";
|
|
853
|
-
function claudeOnPath() {
|
|
854
|
-
try {
|
|
855
|
-
execSync("which claude", { stdio: "pipe" });
|
|
856
|
-
return true;
|
|
857
|
-
} catch {
|
|
858
|
-
return false;
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
async function claudeAuthFileExists() {
|
|
862
|
-
const home = os.homedir();
|
|
863
|
-
const candidates = [`${home}/.claude/.credentials.json`, `${home}/.claude/credentials.json`];
|
|
864
|
-
for (const p of candidates) {
|
|
865
|
-
try {
|
|
866
|
-
await fsp2.stat(p);
|
|
867
|
-
return true;
|
|
868
|
-
} catch {
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
return false;
|
|
872
|
-
}
|
|
873
|
-
async function ollamaHealthy() {
|
|
874
|
-
try {
|
|
875
|
-
const res = await fetch("http://localhost:11434/api/tags", {
|
|
876
|
-
signal: AbortSignal.timeout(300)
|
|
877
|
-
});
|
|
878
|
-
return res.ok;
|
|
879
|
-
} catch {
|
|
880
|
-
return false;
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
async function detectProvider() {
|
|
884
|
-
if (process.env.ANTHROPIC_API_KEY || readEnvValueFromDotEnvLocal("ANTHROPIC_API_KEY")) {
|
|
885
|
-
return { kind: "anthropic-key" };
|
|
886
|
-
}
|
|
887
|
-
if (claudeOnPath() && await claudeAuthFileExists()) {
|
|
888
|
-
return { kind: "claude-cli" };
|
|
889
|
-
}
|
|
890
|
-
if (await ollamaHealthy()) {
|
|
891
|
-
return { kind: "ollama", baseUrl: "http://localhost:11434" };
|
|
892
|
-
}
|
|
893
|
-
if (process.env.OPENAI_API_KEY) {
|
|
894
|
-
return { kind: "openai-key" };
|
|
895
|
-
}
|
|
896
|
-
return {
|
|
897
|
-
kind: "none",
|
|
898
|
-
message: "queque: no AI provider configured\nChecked (in order):\n 1. ANTHROPIC_API_KEY env var or .env.local\n 2. Claude CLI (claude on PATH + credentials file)\n 3. Ollama at http://localhost:11434\n 4. OPENAI_API_KEY env var\nSet one of the above and re-trigger ??"
|
|
899
|
-
};
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
// src/providers/resolver.ts
|
|
903
|
-
function resolveAdapter(detected) {
|
|
904
|
-
switch (detected.kind) {
|
|
905
|
-
case "anthropic-key": {
|
|
906
|
-
const adapter = getProviderAdapter("claude");
|
|
907
|
-
if (!adapter) {
|
|
908
|
-
throw new Error(
|
|
909
|
-
"QueQue: Claude provider is not registered \u2014 was bootstrapBuiltins() called?"
|
|
910
|
-
);
|
|
911
|
-
}
|
|
912
|
-
return adapter;
|
|
913
|
-
}
|
|
914
|
-
case "claude-cli":
|
|
915
|
-
throw new Error(
|
|
916
|
-
"QueQue: Claude CLI detected but subprocess adapter is not wired yet (Phase 8). Set ANTHROPIC_API_KEY or use .env.local."
|
|
917
|
-
);
|
|
918
|
-
case "ollama":
|
|
919
|
-
throw new Error(
|
|
920
|
-
"QueQue: Ollama detected but local adapter is not wired yet (Phase 8). Set ANTHROPIC_API_KEY or use .env.local."
|
|
921
|
-
);
|
|
922
|
-
case "openai-key":
|
|
923
|
-
throw new Error(
|
|
924
|
-
"QueQue: OPENAI_API_KEY detected but OpenAI adapter is not wired yet (Phase 8). Set ANTHROPIC_API_KEY for Claude."
|
|
925
|
-
);
|
|
926
|
-
case "none":
|
|
927
|
-
throw new Error(detected.message);
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
// src/shared/socket-path.ts
|
|
932
|
-
function socketPathForUid(uid) {
|
|
933
|
-
return `/tmp/qq-${uid}.sock`;
|
|
934
|
-
}
|
|
935
|
-
function socketPath() {
|
|
936
|
-
return socketPathForUid(process.getuid?.() ?? 0);
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
// src/ui/CandidateSelect.tsx
|
|
940
|
-
import { Box as Box4, Text as Text4, useInput, useStdin, useStdout as useStdout2 } from "ink";
|
|
941
|
-
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
942
|
-
|
|
943
|
-
// src/ui/ControlsLine.tsx
|
|
944
|
-
import { Box, Text } from "ink";
|
|
945
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
946
|
-
function ControlsLine() {
|
|
947
|
-
return /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
|
|
948
|
-
/* @__PURE__ */ jsx(Text, { backgroundColor: "ansi256(238)", color: "white", bold: true, children: " \u2191\u2193 " }),
|
|
949
|
-
/* @__PURE__ */ jsx(Text, { backgroundColor: "ansi256(245)", color: "black", children: " select " }),
|
|
950
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " }),
|
|
951
|
-
/* @__PURE__ */ jsx(Text, { backgroundColor: "ansi256(238)", color: "white", bold: true, children: " enter " }),
|
|
952
|
-
/* @__PURE__ */ jsx(Text, { backgroundColor: "ansi256(245)", color: "black", children: " accept " }),
|
|
953
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " }),
|
|
954
|
-
/* @__PURE__ */ jsx(Text, { backgroundColor: "ansi256(238)", color: "white", bold: true, children: " esc " }),
|
|
955
|
-
/* @__PURE__ */ jsx(Text, { backgroundColor: "ansi256(245)", color: "black", children: " cancel " })
|
|
956
|
-
] });
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
// src/ui/LoadingSpinner.tsx
|
|
960
|
-
import { Text as Text2 } from "ink";
|
|
961
|
-
import { useEffect, useState } from "react";
|
|
962
|
-
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
963
|
-
var FRAMES = ["thinking\u2026", "thinking.", "thinking..", "thinking\u2026"];
|
|
964
|
-
function LoadingSpinner() {
|
|
965
|
-
const [frame, setFrame] = useState(0);
|
|
966
|
-
useEffect(() => {
|
|
967
|
-
const id = setInterval(() => {
|
|
968
|
-
setFrame((f) => (f + 1) % FRAMES.length);
|
|
969
|
-
}, 200);
|
|
970
|
-
return () => clearInterval(id);
|
|
971
|
-
}, []);
|
|
972
|
-
return /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: FRAMES[frame] });
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
// src/ui/Modal.tsx
|
|
976
|
-
import { Box as Box2, useStdout } from "ink";
|
|
977
|
-
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
978
|
-
function Modal({ children }) {
|
|
979
|
-
const { stdout } = useStdout();
|
|
980
|
-
const width = Math.min(80, stdout.columns || 80);
|
|
981
|
-
return /* @__PURE__ */ jsx3(Box2, { flexDirection: "column", paddingX: 1, width, children });
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
// src/ui/modal-layout.ts
|
|
985
|
-
function wrapText(text, maxWidth) {
|
|
986
|
-
if (maxWidth < 1) {
|
|
987
|
-
return [text];
|
|
988
|
-
}
|
|
989
|
-
const words = text.split(/\s+/).filter((word) => word.length > 0);
|
|
990
|
-
if (words.length === 0) {
|
|
991
|
-
return [""];
|
|
992
|
-
}
|
|
993
|
-
const lines = [];
|
|
994
|
-
let line = "";
|
|
995
|
-
for (const word of words) {
|
|
996
|
-
let remaining = word;
|
|
997
|
-
while (remaining.length > 0) {
|
|
998
|
-
const candidate = line ? `${line} ${remaining}` : remaining;
|
|
999
|
-
if (candidate.length <= maxWidth) {
|
|
1000
|
-
line = candidate;
|
|
1001
|
-
remaining = "";
|
|
1002
|
-
break;
|
|
1003
|
-
}
|
|
1004
|
-
if (line) {
|
|
1005
|
-
lines.push(line);
|
|
1006
|
-
line = "";
|
|
1007
|
-
continue;
|
|
1008
|
-
}
|
|
1009
|
-
lines.push(remaining.slice(0, maxWidth));
|
|
1010
|
-
remaining = remaining.slice(maxWidth);
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
if (line) {
|
|
1014
|
-
lines.push(line);
|
|
1015
|
-
}
|
|
1016
|
-
return lines;
|
|
1017
|
-
}
|
|
1018
|
-
function normalizeCommand(command) {
|
|
1019
|
-
return command.trim();
|
|
1020
|
-
}
|
|
1021
|
-
function filterCandidates(candidates, query) {
|
|
1022
|
-
if (!query) return candidates;
|
|
1023
|
-
const lower = query.toLowerCase();
|
|
1024
|
-
return candidates.filter((c) => normalizeCommand(c.command).toLowerCase().includes(lower));
|
|
1025
|
-
}
|
|
1026
|
-
function estimateCandidateSelectLines(input) {
|
|
1027
|
-
let lines = 0;
|
|
1028
|
-
if (input.initialQuery && input.initialQuery.length > 0) {
|
|
1029
|
-
lines += 1;
|
|
1030
|
-
}
|
|
1031
|
-
lines += 1;
|
|
1032
|
-
lines += 1;
|
|
1033
|
-
if (input.error === true) {
|
|
1034
|
-
lines += 1;
|
|
1035
|
-
} else if (input.candidates === null) {
|
|
1036
|
-
lines += 1;
|
|
1037
|
-
} else {
|
|
1038
|
-
const visible2 = filterCandidates(input.candidates, input.query);
|
|
1039
|
-
if (visible2.length === 0) {
|
|
1040
|
-
lines += 1;
|
|
1041
|
-
} else {
|
|
1042
|
-
for (const candidate of visible2) {
|
|
1043
|
-
const command = normalizeCommand(candidate.command);
|
|
1044
|
-
const commandLines = wrapText(command, input.contentWidth);
|
|
1045
|
-
const explanationLines = candidate.explanation.length > 0 ? wrapText(candidate.explanation, input.contentWidth) : [];
|
|
1046
|
-
lines += commandLines.length + explanationLines.length;
|
|
1047
|
-
lines += 1;
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
const visible = input.candidates !== null && input.error !== true ? filterCandidates(input.candidates, input.query) : [];
|
|
1052
|
-
const selectedCommand = visible[input.selectedIndex]?.command ? normalizeCommand(visible[input.selectedIndex].command) : void 0;
|
|
1053
|
-
const showDestructiveWarning = selectedCommand !== void 0 && isDestructiveCommand(selectedCommand);
|
|
1054
|
-
if (showDestructiveWarning) {
|
|
1055
|
-
lines += 1;
|
|
1056
|
-
lines += 1;
|
|
1057
|
-
}
|
|
1058
|
-
lines += 1;
|
|
1059
|
-
lines += 1;
|
|
1060
|
-
if (input.includeDevFooter) {
|
|
1061
|
-
lines += 1;
|
|
1062
|
-
}
|
|
1063
|
-
return lines;
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
// src/ui/SearchInput.tsx
|
|
1067
|
-
import { Box as Box3, Text as Text3 } from "ink";
|
|
1068
|
-
import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1069
|
-
function SearchInput({ query }) {
|
|
1070
|
-
return /* @__PURE__ */ jsxs2(Box3, { children: [
|
|
1071
|
-
/* @__PURE__ */ jsx4(Text3, { color: "cyan", bold: true, children: "FILTER: " }),
|
|
1072
|
-
query ? /* @__PURE__ */ jsx4(Text3, { children: query }) : /* @__PURE__ */ jsx4(Text3, { dimColor: true, children: "type to filter\u2026" })
|
|
1073
|
-
] });
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
// src/ui/CandidateSelect.tsx
|
|
1077
|
-
import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1078
|
-
var ROW_GUTTER = " ";
|
|
1079
|
-
var ACTIVE_HEAD = "\u250C> ";
|
|
1080
|
-
var ACTIVE_TAIL = "\u2502 ";
|
|
1081
|
-
var DESTRUCTIVE_WARNING_MARK = "\u26A0";
|
|
1082
|
-
var DESTRUCTIVE_WARNING_TEXT = `${DESTRUCTIVE_WARNING_MARK} review carefully \u2014 this command may be destructive`;
|
|
1083
|
-
var MODAL_MAX_WIDTH = 80;
|
|
1084
|
-
var MODAL_PADDING_X = 2;
|
|
1085
|
-
function linePrefix(active, lineIndex) {
|
|
1086
|
-
if (!active) {
|
|
1087
|
-
return ROW_GUTTER;
|
|
1088
|
-
}
|
|
1089
|
-
return lineIndex === 0 ? ACTIVE_HEAD : ACTIVE_TAIL;
|
|
1090
|
-
}
|
|
1091
|
-
function normalizeCommand2(command) {
|
|
1092
|
-
return command.trim();
|
|
1093
|
-
}
|
|
1094
|
-
function CandidateSelect({
|
|
1095
|
-
candidates,
|
|
1096
|
-
initialQuery,
|
|
1097
|
-
onSelect,
|
|
1098
|
-
onCancel,
|
|
1099
|
-
error,
|
|
1100
|
-
onLayoutLinesChange
|
|
1101
|
-
}) {
|
|
1102
|
-
const { isRawModeSupported } = useStdin();
|
|
1103
|
-
const { stdout } = useStdout2();
|
|
1104
|
-
const modalWidth = Math.min(MODAL_MAX_WIDTH, stdout.columns || MODAL_MAX_WIDTH);
|
|
1105
|
-
const contentWidth = modalWidth - MODAL_PADDING_X - ROW_GUTTER.length;
|
|
1106
|
-
const [selectedIndex, setSelectedIndex] = useState2(0);
|
|
1107
|
-
const [query, setQuery] = useState2(initialQuery ?? "");
|
|
1108
|
-
useEffect2(() => {
|
|
1109
|
-
if (!isRawModeSupported) {
|
|
1110
|
-
onCancel();
|
|
1111
|
-
}
|
|
1112
|
-
}, [isRawModeSupported, onCancel]);
|
|
1113
|
-
useEffect2(() => {
|
|
1114
|
-
setSelectedIndex(0);
|
|
1115
|
-
if (candidates && candidates.length > 0 && filterCandidates(candidates, query).length === 0) {
|
|
1116
|
-
setQuery("");
|
|
1117
|
-
}
|
|
1118
|
-
}, [candidates]);
|
|
1119
|
-
useEffect2(() => {
|
|
1120
|
-
setSelectedIndex(0);
|
|
1121
|
-
}, [query]);
|
|
1122
|
-
const includeDevFooter = process.env.QQ_DEV_ROOT != null;
|
|
1123
|
-
useEffect2(() => {
|
|
1124
|
-
if (!onLayoutLinesChange) {
|
|
17
|
+
if (process.env.NODE_COMPILE_CACHE !== void 0 || process.env.NODE_DISABLE_COMPILE_CACHE !== void 0) {
|
|
1125
18
|
return;
|
|
1126
19
|
}
|
|
1127
|
-
|
|
20
|
+
const enableCompileCache2 = nodeModule.enableCompileCache;
|
|
21
|
+
if (typeof enableCompileCache2 !== "function" || typeof process.getuid !== "function") {
|
|
1128
22
|
return;
|
|
1129
23
|
}
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
contentWidth,
|
|
1138
|
-
includeDevFooter
|
|
1139
|
-
})
|
|
1140
|
-
);
|
|
1141
|
-
}, [
|
|
1142
|
-
candidates,
|
|
1143
|
-
initialQuery,
|
|
1144
|
-
query,
|
|
1145
|
-
selectedIndex,
|
|
1146
|
-
error,
|
|
1147
|
-
contentWidth,
|
|
1148
|
-
includeDevFooter,
|
|
1149
|
-
onLayoutLinesChange
|
|
1150
|
-
]);
|
|
1151
|
-
useInput(
|
|
1152
|
-
(input, key) => {
|
|
1153
|
-
if (key.escape) {
|
|
1154
|
-
onCancel();
|
|
1155
|
-
return;
|
|
1156
|
-
}
|
|
1157
|
-
if (key.backspace) {
|
|
1158
|
-
setQuery((q) => q.slice(0, -1));
|
|
1159
|
-
return;
|
|
1160
|
-
}
|
|
1161
|
-
if (key.return && candidates) {
|
|
1162
|
-
const visible = filterCandidates(candidates, query);
|
|
1163
|
-
if (visible.length === 0) return;
|
|
1164
|
-
const selected = visible[selectedIndex] ?? visible[0];
|
|
1165
|
-
const command = normalizeCommand2(selected.command);
|
|
1166
|
-
const explanation = selected.explanation || `see man ${command.split(" ")[0]}`;
|
|
1167
|
-
onSelect(command, explanation);
|
|
1168
|
-
return;
|
|
1169
|
-
}
|
|
1170
|
-
if (input && !key.ctrl && !key.meta) {
|
|
1171
|
-
setQuery((q) => q + input);
|
|
1172
|
-
return;
|
|
1173
|
-
}
|
|
1174
|
-
if (key.upArrow && candidates) {
|
|
1175
|
-
const visible = filterCandidates(candidates, query);
|
|
1176
|
-
setSelectedIndex((i) => i === 0 ? visible.length - 1 : i - 1);
|
|
1177
|
-
return;
|
|
1178
|
-
}
|
|
1179
|
-
if (key.downArrow && candidates) {
|
|
1180
|
-
const visible = filterCandidates(candidates, query);
|
|
1181
|
-
setSelectedIndex((i) => (i + 1) % visible.length);
|
|
1182
|
-
return;
|
|
1183
|
-
}
|
|
1184
|
-
},
|
|
1185
|
-
{ isActive: isRawModeSupported }
|
|
1186
|
-
);
|
|
1187
|
-
let content;
|
|
1188
|
-
let showDestructiveWarning = false;
|
|
1189
|
-
if (error === true) {
|
|
1190
|
-
content = /* @__PURE__ */ jsx5(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text4, { dimColor: true, children: "provider error \u2014 press esc to return" }) });
|
|
1191
|
-
} else if (candidates === null) {
|
|
1192
|
-
content = /* @__PURE__ */ jsx5(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx5(LoadingSpinner, {}) });
|
|
1193
|
-
} else {
|
|
1194
|
-
const visible = filterCandidates(candidates, query);
|
|
1195
|
-
const selectedCommand = visible[selectedIndex]?.command ? normalizeCommand2(visible[selectedIndex].command) : void 0;
|
|
1196
|
-
showDestructiveWarning = selectedCommand !== void 0 && isDestructiveCommand(selectedCommand);
|
|
1197
|
-
if (visible.length === 0) {
|
|
1198
|
-
content = /* @__PURE__ */ jsx5(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text4, { dimColor: true, children: "no matches \u2014 try a different query" }) });
|
|
1199
|
-
} else {
|
|
1200
|
-
content = /* @__PURE__ */ jsx5(Box4, { flexDirection: "column", marginTop: 1, children: visible.map((candidate, index) => {
|
|
1201
|
-
const active = index === selectedIndex;
|
|
1202
|
-
const command = normalizeCommand2(candidate.command);
|
|
1203
|
-
const destructive = isDestructiveCommand(command);
|
|
1204
|
-
const commandLines = wrapText(command, contentWidth);
|
|
1205
|
-
const explanationLines = candidate.explanation.length > 0 ? wrapText(candidate.explanation, contentWidth) : [];
|
|
1206
|
-
const blockLines = [...commandLines, ...explanationLines];
|
|
1207
|
-
return /* @__PURE__ */ jsx5(
|
|
1208
|
-
Box4,
|
|
1209
|
-
{
|
|
1210
|
-
flexDirection: "column",
|
|
1211
|
-
marginBottom: 1,
|
|
1212
|
-
children: blockLines.map((line, lineIndex) => {
|
|
1213
|
-
const isCommandLine = lineIndex < commandLines.length;
|
|
1214
|
-
const isLastCommandLine = isCommandLine && lineIndex === commandLines.length - 1;
|
|
1215
|
-
const prefix = linePrefix(active, lineIndex);
|
|
1216
|
-
return /* @__PURE__ */ jsxs3(Box4, { children: [
|
|
1217
|
-
/* @__PURE__ */ jsx5(Text4, { color: active ? "ansi256(166)" : void 0, children: prefix }),
|
|
1218
|
-
/* @__PURE__ */ jsx5(
|
|
1219
|
-
Text4,
|
|
1220
|
-
{
|
|
1221
|
-
bold: active && isCommandLine,
|
|
1222
|
-
color: active && isCommandLine ? "white" : void 0,
|
|
1223
|
-
dimColor: !active || !isCommandLine,
|
|
1224
|
-
children: line
|
|
1225
|
-
}
|
|
1226
|
-
),
|
|
1227
|
-
isLastCommandLine && destructive && /* @__PURE__ */ jsx5(Text4, { color: "yellow", children: ` ${DESTRUCTIVE_WARNING_MARK}` })
|
|
1228
|
-
] }, `${prefix}${line}`);
|
|
1229
|
-
})
|
|
1230
|
-
},
|
|
1231
|
-
`${command}::${candidate.explanation}`
|
|
1232
|
-
);
|
|
1233
|
-
}) });
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
return /* @__PURE__ */ jsxs3(Modal, { children: [
|
|
1237
|
-
initialQuery && initialQuery.length > 0 && /* @__PURE__ */ jsx5(Box4, { flexGrow: 0, height: 1, children: /* @__PURE__ */ jsx5(Text4, { dimColor: true, wrap: "truncate", children: `queque \u203A ${initialQuery}` }) }),
|
|
1238
|
-
/* @__PURE__ */ jsx5(SearchInput, { query }),
|
|
1239
|
-
content,
|
|
1240
|
-
showDestructiveWarning && /* @__PURE__ */ jsx5(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text4, { color: "yellow", children: DESTRUCTIVE_WARNING_TEXT }) }),
|
|
1241
|
-
/* @__PURE__ */ jsx5(ControlsLine, {}),
|
|
1242
|
-
process.env.QQ_DEV_ROOT != null && /* @__PURE__ */ jsx5(Box4, { justifyContent: "flex-end", children: /* @__PURE__ */ jsx5(Text4, { color: "yellow", children: `dev v${"0.4.2"} ${"bad1456"}` }) })
|
|
1243
|
-
] });
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
// src/client/result-writer.ts
|
|
1247
|
-
import * as fsp3 from "fs/promises";
|
|
1248
|
-
async function writeShellResult(resultFile, result) {
|
|
1249
|
-
const parsed = shellResultSchema.parse(result);
|
|
1250
|
-
const line = `${JSON.stringify(parsed)}
|
|
1251
|
-
`;
|
|
1252
|
-
let isFifo = false;
|
|
1253
|
-
try {
|
|
1254
|
-
const st = await fsp3.stat(resultFile);
|
|
1255
|
-
isFifo = st.isFIFO();
|
|
1256
|
-
} catch {
|
|
1257
|
-
}
|
|
1258
|
-
if (isFifo) {
|
|
1259
|
-
await fsp3.writeFile(resultFile, line, { encoding: "utf-8" });
|
|
1260
|
-
} else {
|
|
1261
|
-
const tmpFile = `${resultFile}.tmp`;
|
|
1262
|
-
await fsp3.writeFile(tmpFile, line, { encoding: "utf-8" });
|
|
1263
|
-
await fsp3.rename(tmpFile, resultFile);
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
// src/client/zellij-pane-resize.ts
|
|
1268
|
-
import { spawn as spawn2 } from "child_process";
|
|
1269
|
-
var DEFAULT_MAX_PANE_HEIGHT = 24;
|
|
1270
|
-
var MIN_PANE_HEIGHT = 6;
|
|
1271
|
-
var ZELLIJ_FLOATING_FRAME_LINES = 2;
|
|
1272
|
-
var lastSyncedHeight;
|
|
1273
|
-
function readMaxZellijPaneHeight() {
|
|
1274
|
-
const parsed = Number.parseInt(process.env.QQ_PANE_HEIGHT ?? "", 10);
|
|
1275
|
-
if (Number.isFinite(parsed) && parsed >= MIN_PANE_HEIGHT) {
|
|
1276
|
-
return parsed;
|
|
1277
|
-
}
|
|
1278
|
-
return DEFAULT_MAX_PANE_HEIGHT;
|
|
1279
|
-
}
|
|
1280
|
-
function resolveZellijPaneHeight(contentLines, maxHeight = readMaxZellijPaneHeight()) {
|
|
1281
|
-
const withFrame = contentLines + ZELLIJ_FLOATING_FRAME_LINES;
|
|
1282
|
-
return Math.min(Math.max(withFrame, MIN_PANE_HEIGHT), maxHeight);
|
|
1283
|
-
}
|
|
1284
|
-
function formatZellijPaneId(rawPaneId) {
|
|
1285
|
-
if (rawPaneId.startsWith("terminal_") || rawPaneId.startsWith("plugin_")) {
|
|
1286
|
-
return rawPaneId;
|
|
1287
|
-
}
|
|
1288
|
-
return `terminal_${rawPaneId}`;
|
|
1289
|
-
}
|
|
1290
|
-
function syncZellijFloatingPaneHeight(contentLines) {
|
|
1291
|
-
if (process.env.ZELLIJ === void 0) {
|
|
1292
|
-
return;
|
|
1293
|
-
}
|
|
1294
|
-
const rawPaneId = process.env.ZELLIJ_PANE_ID;
|
|
1295
|
-
if (rawPaneId === void 0 || rawPaneId.length === 0) {
|
|
1296
|
-
return;
|
|
1297
|
-
}
|
|
1298
|
-
const height = resolveZellijPaneHeight(contentLines);
|
|
1299
|
-
if (height === lastSyncedHeight) {
|
|
1300
|
-
return;
|
|
1301
|
-
}
|
|
1302
|
-
lastSyncedHeight = height;
|
|
1303
|
-
const paneId = formatZellijPaneId(rawPaneId);
|
|
1304
|
-
const child = spawn2(
|
|
1305
|
-
"zellij",
|
|
1306
|
-
["action", "change-floating-pane-coordinates", "--pane-id", paneId, "--height", String(height)],
|
|
1307
|
-
{ stdio: "ignore" }
|
|
1308
|
-
);
|
|
1309
|
-
child.unref();
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
// src/client/run-foreground.ts
|
|
1313
|
-
function buildShellBuffers(command, explanation) {
|
|
1314
|
-
const comment = ` # ${explanation}`;
|
|
1315
|
-
const stripped = command.replace(/<([^>]+)>/g, "$1");
|
|
1316
|
-
const firstAngle = command.indexOf("<");
|
|
1317
|
-
if (firstAngle >= 0) {
|
|
1318
|
-
return {
|
|
1319
|
-
lbuffer: stripped.slice(0, firstAngle),
|
|
1320
|
-
rbuffer: stripped.slice(firstAngle) + comment
|
|
1321
|
-
};
|
|
1322
|
-
}
|
|
1323
|
-
return { lbuffer: command, rbuffer: comment };
|
|
1324
|
-
}
|
|
1325
|
-
async function runForegroundClient(args) {
|
|
1326
|
-
const { requestFile, resultFile, resultMode } = args;
|
|
1327
|
-
const uid = typeof process.getuid === "function" ? process.getuid() : 0;
|
|
1328
|
-
const socketPath2 = socketPathForUid(uid);
|
|
1329
|
-
const inZellij = process.env.ZELLIJ !== void 0;
|
|
1330
|
-
void appendDebugLog("client", "foreground start", {
|
|
1331
|
-
requestFile,
|
|
1332
|
-
resultFile,
|
|
1333
|
-
resultMode,
|
|
1334
|
-
socketPath: socketPath2,
|
|
1335
|
-
inZellij
|
|
1336
|
-
});
|
|
1337
|
-
const ttyHandle = await fsp4.open("/dev/tty", "r+").catch(() => null);
|
|
1338
|
-
try {
|
|
1339
|
-
const raw = await fsp4.readFile(requestFile, "utf-8");
|
|
1340
|
-
const request = shellRequestSchema.parse(JSON.parse(raw.trim()));
|
|
1341
|
-
void appendDebugLog("client", "request parsed", {
|
|
1342
|
-
lbufferLength: request.lbuffer.length,
|
|
1343
|
-
rbufferLength: request.rbuffer.length,
|
|
1344
|
-
cwd: request.cwd,
|
|
1345
|
-
shellPid: request.shellPid
|
|
1346
|
-
});
|
|
1347
|
-
const detectedProvider = await detectProvider();
|
|
1348
|
-
void appendDebugLog("client", "provider detected", { kind: detectedProvider.kind });
|
|
1349
|
-
if (detectedProvider.kind === "none") {
|
|
1350
|
-
if (ttyHandle && !inZellij) {
|
|
1351
|
-
try {
|
|
1352
|
-
await ttyHandle.write(`\r
|
|
1353
|
-
${detectedProvider.message}\r
|
|
1354
|
-
`);
|
|
1355
|
-
} catch {
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
await writeShellResult(resultFile, { kind: "error", message: detectedProvider.message });
|
|
24
|
+
const xdgCacheHome = process.env.XDG_CACHE_HOME;
|
|
25
|
+
const cacheRoot = xdgCacheHome !== void 0 && isAbsolute(xdgCacheHome) ? xdgCacheHome : join(homedir(), ".cache");
|
|
26
|
+
const appDir = join(cacheRoot, "queque");
|
|
27
|
+
const cacheDir = join(appDir, "node-compile-cache");
|
|
28
|
+
mkdirSync(cacheDir, { recursive: true, mode: 448 });
|
|
29
|
+
const uid = process.getuid();
|
|
30
|
+
if (!isPrivateDirectory(appDir, uid) || !isPrivateDirectory(cacheDir, uid)) {
|
|
1359
31
|
return;
|
|
1360
32
|
}
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
if (resultMode === "replace-buffer-fixture") {
|
|
1364
|
-
const fixtureLbuffer = request.lbuffer || "echo hello";
|
|
1365
|
-
await writeShellResult(resultFile, {
|
|
1366
|
-
kind: "replace-buffer",
|
|
1367
|
-
lbuffer: fixtureLbuffer,
|
|
1368
|
-
rbuffer: request.rbuffer
|
|
1369
|
-
});
|
|
1370
|
-
void appendDebugLog("client", "wrote replace-buffer result", {
|
|
1371
|
-
resultFile,
|
|
1372
|
-
lbufferLength: fixtureLbuffer.length,
|
|
1373
|
-
rbufferLength: request.rbuffer.length
|
|
1374
|
-
});
|
|
1375
|
-
} else if (resultMode === "llm") {
|
|
1376
|
-
let ttyReadStream;
|
|
1377
|
-
let ttyWriteStream;
|
|
1378
|
-
try {
|
|
1379
|
-
const decision = classifyIntent({ ...request, intent: "unknown", confidence: 0 });
|
|
1380
|
-
void appendDebugLog("client", "intent classified", {
|
|
1381
|
-
intent: decision.intent,
|
|
1382
|
-
signals: decision.signals
|
|
1383
|
-
});
|
|
1384
|
-
const normalized = {
|
|
1385
|
-
...request,
|
|
1386
|
-
intent: decision.intent,
|
|
1387
|
-
confidence: decision.confidence
|
|
1388
|
-
};
|
|
1389
|
-
const envelope = await gatherContext(normalized);
|
|
1390
|
-
void appendDebugLog("client", "context gathered", {
|
|
1391
|
-
extraCount: envelope.extras.length
|
|
1392
|
-
});
|
|
1393
|
-
const adapter = resolveAdapter(detectedProvider);
|
|
1394
|
-
void appendDebugLog("client", "provider adapter resolved", { kind: detectedProvider.kind });
|
|
1395
|
-
if (ttyHandle) {
|
|
1396
|
-
try {
|
|
1397
|
-
ttyReadStream = new tty.ReadStream(ttyHandle.fd);
|
|
1398
|
-
ttyWriteStream = new tty.WriteStream(ttyHandle.fd);
|
|
1399
|
-
} catch {
|
|
1400
|
-
}
|
|
1401
|
-
}
|
|
1402
|
-
if (!ttyReadStream || !ttyWriteStream) {
|
|
1403
|
-
const msg = "\r\nqueque: could not open terminal for interactive input\r\n";
|
|
1404
|
-
if (ttyHandle) {
|
|
1405
|
-
try {
|
|
1406
|
-
await ttyHandle.write(msg);
|
|
1407
|
-
} catch {
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
await writeShellResult(resultFile, { kind: "error", message: msg.trim() });
|
|
1411
|
-
return;
|
|
1412
|
-
}
|
|
1413
|
-
try {
|
|
1414
|
-
ttyReadStream.setRawMode(true);
|
|
1415
|
-
ttyReadStream.setRawMode(false);
|
|
1416
|
-
} catch {
|
|
1417
|
-
const msg = "\r\nqueque: terminal does not support interactive mode\r\nRun ?? from an interactive ZSH session.\r\n";
|
|
1418
|
-
ttyWriteStream.write(msg);
|
|
1419
|
-
await writeShellResult(resultFile, {
|
|
1420
|
-
kind: "error",
|
|
1421
|
-
message: "queque: terminal does not support interactive mode \u2014 run ?? from an interactive shell"
|
|
1422
|
-
});
|
|
1423
|
-
return;
|
|
1424
|
-
}
|
|
1425
|
-
const MODAL_VIEWPORT_LINES = 16;
|
|
1426
|
-
if (ttyWriteStream && !inZellij) {
|
|
1427
|
-
ttyWriteStream.write("\n".repeat(MODAL_VIEWPORT_LINES));
|
|
1428
|
-
ttyWriteStream.write(`\x1B[${MODAL_VIEWPORT_LINES}A`);
|
|
1429
|
-
ttyWriteStream.write("\x1B7");
|
|
1430
|
-
}
|
|
1431
|
-
await new Promise((resolve3) => {
|
|
1432
|
-
let unmount;
|
|
1433
|
-
let resolved = false;
|
|
1434
|
-
const buildCandidateElement = (candidates, errorState) => React.createElement(CandidateSelect, {
|
|
1435
|
-
candidates,
|
|
1436
|
-
initialQuery: request.lbuffer,
|
|
1437
|
-
error: errorState,
|
|
1438
|
-
onSelect: async (command, explanation) => {
|
|
1439
|
-
if (resolved) return;
|
|
1440
|
-
resolved = true;
|
|
1441
|
-
const { lbuffer, rbuffer } = buildShellBuffers(command, explanation);
|
|
1442
|
-
await writeShellResult(resultFile, {
|
|
1443
|
-
kind: "replace-buffer",
|
|
1444
|
-
lbuffer,
|
|
1445
|
-
rbuffer,
|
|
1446
|
-
query: request.lbuffer
|
|
1447
|
-
});
|
|
1448
|
-
unmount?.();
|
|
1449
|
-
},
|
|
1450
|
-
onCancel: async () => {
|
|
1451
|
-
if (resolved) return;
|
|
1452
|
-
resolved = true;
|
|
1453
|
-
await writeShellResult(resultFile, { kind: "cancel" });
|
|
1454
|
-
unmount?.();
|
|
1455
|
-
},
|
|
1456
|
-
onLayoutLinesChange: inZellij ? syncZellijFloatingPaneHeight : void 0
|
|
1457
|
-
});
|
|
1458
|
-
const renderOptions = ttyReadStream && ttyWriteStream ? { stdin: ttyReadStream, stdout: ttyWriteStream, interactive: true } : { interactive: true };
|
|
1459
|
-
const app = render(buildCandidateElement(null), renderOptions);
|
|
1460
|
-
const clearScrollReserve = () => {
|
|
1461
|
-
if (ttyWriteStream && !inZellij) {
|
|
1462
|
-
try {
|
|
1463
|
-
ttyWriteStream.write("\x1B8\x1B[J");
|
|
1464
|
-
} catch {
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
};
|
|
1468
|
-
const cleanupOnSignal = () => {
|
|
1469
|
-
app.unmount();
|
|
1470
|
-
clearScrollReserve();
|
|
1471
|
-
process.exit(0);
|
|
1472
|
-
};
|
|
1473
|
-
process.once("SIGHUP", cleanupOnSignal);
|
|
1474
|
-
process.once("SIGTERM", cleanupOnSignal);
|
|
1475
|
-
unmount = (skipClear = false) => {
|
|
1476
|
-
process.off("SIGHUP", cleanupOnSignal);
|
|
1477
|
-
process.off("SIGTERM", cleanupOnSignal);
|
|
1478
|
-
app.unmount();
|
|
1479
|
-
if (!skipClear) clearScrollReserve();
|
|
1480
|
-
resolve3();
|
|
1481
|
-
};
|
|
1482
|
-
adapter.fetchCandidates(envelope).then((candidates) => {
|
|
1483
|
-
void appendDebugLog("client", "candidates received", { count: candidates.length });
|
|
1484
|
-
app.rerender(buildCandidateElement(candidates));
|
|
1485
|
-
}).catch(async (err) => {
|
|
1486
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1487
|
-
void appendDebugLog("client", "llm request failed", { message });
|
|
1488
|
-
if (resolved) return;
|
|
1489
|
-
resolved = true;
|
|
1490
|
-
const errorMsg = `QueQue: ${message}`;
|
|
1491
|
-
await writeShellResult(resultFile, { kind: "error", message: errorMsg });
|
|
1492
|
-
unmount?.(true);
|
|
1493
|
-
});
|
|
1494
|
-
});
|
|
1495
|
-
void appendDebugLog("client", "wrote llm result", { resultFile });
|
|
1496
|
-
} catch (error) {
|
|
1497
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1498
|
-
void appendDebugLog("client", "llm request failed", { message });
|
|
1499
|
-
const errorMsg = `QueQue: ${message}`;
|
|
1500
|
-
await writeShellResult(resultFile, { kind: "error", message: errorMsg });
|
|
1501
|
-
}
|
|
1502
|
-
} else {
|
|
1503
|
-
await writeShellResult(resultFile, { kind: "cancel" });
|
|
1504
|
-
void appendDebugLog("client", "wrote cancel result", { resultFile });
|
|
1505
|
-
}
|
|
1506
|
-
} finally {
|
|
1507
|
-
await ttyHandle?.close();
|
|
1508
|
-
}
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
|
-
// src/cli/commands/client.ts
|
|
1512
|
-
function parseResultMode(mode) {
|
|
1513
|
-
if (mode === void 0 || mode === "llm") return "llm";
|
|
1514
|
-
if (mode === "cancel") return "cancel";
|
|
1515
|
-
if (mode === "replace-buffer-fixture") return "replace-buffer-fixture";
|
|
1516
|
-
void appendDebugLog("client", `unknown --result-mode "${mode}", defaulting to cancel`);
|
|
1517
|
-
return "cancel";
|
|
1518
|
-
}
|
|
1519
|
-
async function clientCommand(options) {
|
|
1520
|
-
const resultMode = parseResultMode(options.resultMode);
|
|
1521
|
-
void appendDebugLog("client", "command start", {
|
|
1522
|
-
requestFile: options.requestFile,
|
|
1523
|
-
resultFile: options.resultFile,
|
|
1524
|
-
resultMode
|
|
1525
|
-
});
|
|
1526
|
-
await runForegroundClient({
|
|
1527
|
-
requestFile: options.requestFile,
|
|
1528
|
-
resultFile: options.resultFile,
|
|
1529
|
-
resultMode
|
|
1530
|
-
});
|
|
1531
|
-
}
|
|
1532
|
-
|
|
1533
|
-
// src/daemon/server.ts
|
|
1534
|
-
import * as net2 from "net";
|
|
1535
|
-
|
|
1536
|
-
// src/contracts/ipc.ts
|
|
1537
|
-
import { z as z5 } from "zod";
|
|
1538
|
-
var ipcRequestSchema = z5.discriminatedUnion("kind", [
|
|
1539
|
-
z5.object({
|
|
1540
|
-
kind: z5.literal("ping")
|
|
1541
|
-
}),
|
|
1542
|
-
z5.object({
|
|
1543
|
-
kind: z5.literal("ensure-session"),
|
|
1544
|
-
shellPid: z5.number().int().positive(),
|
|
1545
|
-
ttyPath: z5.string()
|
|
1546
|
-
}),
|
|
1547
|
-
z5.object({
|
|
1548
|
-
kind: z5.literal("run-query"),
|
|
1549
|
-
request: shellRequestSchema
|
|
1550
|
-
})
|
|
1551
|
-
]);
|
|
1552
|
-
var ipcResponseSchema = z5.discriminatedUnion("kind", [
|
|
1553
|
-
z5.object({
|
|
1554
|
-
kind: z5.literal("pong")
|
|
1555
|
-
}),
|
|
1556
|
-
z5.object({
|
|
1557
|
-
kind: z5.literal("session-ready"),
|
|
1558
|
-
socketPath: z5.string()
|
|
1559
|
-
}),
|
|
1560
|
-
z5.object({
|
|
1561
|
-
kind: z5.literal("query-accepted"),
|
|
1562
|
-
requestId: z5.string()
|
|
1563
|
-
})
|
|
1564
|
-
]);
|
|
1565
|
-
|
|
1566
|
-
// src/daemon/server.ts
|
|
1567
|
-
function startDaemonServer(socketPath2) {
|
|
1568
|
-
return new Promise((resolve3, reject) => {
|
|
1569
|
-
const MAX_BUF_BYTES = 64 * 1024;
|
|
1570
|
-
const server = net2.createServer((socket) => {
|
|
1571
|
-
let buf = "";
|
|
1572
|
-
void appendDebugLog("daemon", "socket connected", { socketPath: socketPath2 });
|
|
1573
|
-
socket.on("data", (chunk) => {
|
|
1574
|
-
if (buf.length + chunk.length > MAX_BUF_BYTES) {
|
|
1575
|
-
socket.destroy(new Error("IPC message too large"));
|
|
1576
|
-
return;
|
|
1577
|
-
}
|
|
1578
|
-
buf += chunk.toString();
|
|
1579
|
-
let nl = buf.indexOf("\n");
|
|
1580
|
-
while (nl !== -1) {
|
|
1581
|
-
const line = buf.slice(0, nl).trim();
|
|
1582
|
-
buf = buf.slice(nl + 1);
|
|
1583
|
-
nl = buf.indexOf("\n");
|
|
1584
|
-
if (!line) continue;
|
|
1585
|
-
let parsed;
|
|
1586
|
-
try {
|
|
1587
|
-
parsed = JSON.parse(line);
|
|
1588
|
-
} catch {
|
|
1589
|
-
continue;
|
|
1590
|
-
}
|
|
1591
|
-
const result = ipcRequestSchema.safeParse(parsed);
|
|
1592
|
-
if (!result.success) {
|
|
1593
|
-
void appendDebugLog("daemon", "ignored unknown request", { line });
|
|
1594
|
-
continue;
|
|
1595
|
-
}
|
|
1596
|
-
const req = result.data;
|
|
1597
|
-
void appendDebugLog("daemon", "request received", req);
|
|
1598
|
-
switch (req.kind) {
|
|
1599
|
-
case "ping": {
|
|
1600
|
-
socket.write(`${JSON.stringify({ kind: "pong" })}
|
|
1601
|
-
`);
|
|
1602
|
-
void appendDebugLog("daemon", "replied pong");
|
|
1603
|
-
break;
|
|
1604
|
-
}
|
|
1605
|
-
case "ensure-session": {
|
|
1606
|
-
socket.write(`${JSON.stringify({ kind: "session-ready", socketPath: socketPath2 })}
|
|
1607
|
-
`);
|
|
1608
|
-
void appendDebugLog("daemon", "replied session-ready", { socketPath: socketPath2 });
|
|
1609
|
-
break;
|
|
1610
|
-
}
|
|
1611
|
-
case "run-query": {
|
|
1612
|
-
const requestId = Math.random().toString(36).slice(2);
|
|
1613
|
-
socket.write(`${JSON.stringify({ kind: "query-accepted", requestId })}
|
|
1614
|
-
`);
|
|
1615
|
-
void appendDebugLog("daemon", "replied query-accepted", { requestId });
|
|
1616
|
-
break;
|
|
1617
|
-
}
|
|
1618
|
-
}
|
|
1619
|
-
}
|
|
1620
|
-
});
|
|
1621
|
-
socket.on("error", () => {
|
|
1622
|
-
});
|
|
1623
|
-
});
|
|
1624
|
-
server.on("error", reject);
|
|
1625
|
-
server.listen(socketPath2, () => {
|
|
1626
|
-
void appendDebugLog("daemon", "listening", { socketPath: socketPath2 });
|
|
1627
|
-
resolve3(server);
|
|
1628
|
-
});
|
|
1629
|
-
});
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
// src/cli/commands/daemon.ts
|
|
1633
|
-
async function daemonCommand(options) {
|
|
1634
|
-
const sock = options.socket ?? socketPath();
|
|
1635
|
-
void appendDebugLog("daemon", "command start", { socket: sock, ensure: options.ensure });
|
|
1636
|
-
if (options.ensure) {
|
|
1637
|
-
await ensureDaemon(sock);
|
|
1638
|
-
void appendDebugLog("daemon", "ensure command complete", { socket: sock });
|
|
1639
|
-
return;
|
|
1640
|
-
}
|
|
1641
|
-
const server = await startDaemonServer(sock);
|
|
1642
|
-
void appendDebugLog("daemon", "server started", { socket: sock });
|
|
1643
|
-
const shutdown = () => {
|
|
1644
|
-
void appendDebugLog("daemon", "shutdown", { socket: sock });
|
|
1645
|
-
server.close(() => {
|
|
1646
|
-
process.exit(0);
|
|
1647
|
-
});
|
|
1648
|
-
};
|
|
1649
|
-
process.once("SIGTERM", shutdown);
|
|
1650
|
-
process.once("SIGINT", shutdown);
|
|
1651
|
-
await new Promise(() => {
|
|
1652
|
-
});
|
|
1653
|
-
}
|
|
1654
|
-
|
|
1655
|
-
// src/cli/commands/init.ts
|
|
1656
|
-
import { appendFileSync, existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
1657
|
-
import { homedir as homedir3 } from "os";
|
|
1658
|
-
import { dirname as dirname3, join as join3, resolve as resolve2 } from "path";
|
|
1659
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1660
|
-
var __dirname = dirname3(fileURLToPath2(import.meta.url));
|
|
1661
|
-
var HOMEBREW_PREFIXES = ["/opt/homebrew", "/usr/local", "/home/linuxbrew/.linuxbrew"];
|
|
1662
|
-
function _resolveScriptPath(shell) {
|
|
1663
|
-
for (const prefix of HOMEBREW_PREFIXES) {
|
|
1664
|
-
const p = resolve2(prefix, "share", "queque", `queque.${shell}`);
|
|
1665
|
-
if (existsSync3(p)) return p;
|
|
1666
|
-
}
|
|
1667
|
-
return resolve2(__dirname, "../../shell", shell, `queque.${shell}`);
|
|
1668
|
-
}
|
|
1669
|
-
function hasShellIntegration(existing, shell) {
|
|
1670
|
-
if (/# queque shell integration/.test(existing)) {
|
|
1671
|
-
return true;
|
|
33
|
+
enableCompileCache2(cacheDir);
|
|
34
|
+
} catch {
|
|
1672
35
|
}
|
|
1673
|
-
const scriptPattern = new RegExp(`source\\s+["'][^"']*queque\\.${shell.replace(".", "\\.")}["']`);
|
|
1674
|
-
return scriptPattern.test(existing);
|
|
1675
36
|
}
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
}
|
|
1683
|
-
const zshrc = join3(homedir3(), ".zshrc");
|
|
1684
|
-
const scriptPath = _resolveScriptPath(shell);
|
|
1685
|
-
const snippet = `source "${scriptPath}"`;
|
|
1686
|
-
const existing = existsSync3(zshrc) ? readFileSync3(zshrc, "utf8") : "";
|
|
1687
|
-
if (hasShellIntegration(existing, shell)) {
|
|
1688
|
-
console.error("queque: shell integration already present in ~/.zshrc");
|
|
1689
|
-
process.exit(0);
|
|
1690
|
-
}
|
|
1691
|
-
appendFileSync(zshrc, `
|
|
1692
|
-
# queque shell integration
|
|
1693
|
-
${snippet}
|
|
37
|
+
enablePerUserCompileCache();
|
|
38
|
+
var appUrl = new URL("./app.js", import.meta.url).href;
|
|
39
|
+
try {
|
|
40
|
+
const app = await import(appUrl);
|
|
41
|
+
await app.main();
|
|
42
|
+
} catch (error) {
|
|
43
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}
|
|
1694
44
|
`);
|
|
1695
|
-
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
|
-
// src/cli/main.ts
|
|
1699
|
-
function requiredOption(name, value) {
|
|
1700
|
-
if (!value) {
|
|
1701
|
-
throw new Error(`Missing required option: ${name}`);
|
|
1702
|
-
}
|
|
1703
|
-
return value;
|
|
1704
|
-
}
|
|
1705
|
-
var QQ_RESULT_FILE_PATTERN = /^\/tmp\/qq-sess\.[A-Za-z0-9]+\//;
|
|
1706
|
-
process.on("uncaughtException", (err) => {
|
|
1707
|
-
console.error("QueQue: uncaught exception:", err.message);
|
|
1708
|
-
const resultFile = process.env.QQ_RESULT_FILE;
|
|
1709
|
-
if (resultFile && QQ_RESULT_FILE_PATTERN.test(resultFile)) {
|
|
1710
|
-
try {
|
|
1711
|
-
fs2.writeFileSync(resultFile, '{"kind":"cancel"}\n');
|
|
1712
|
-
} catch {
|
|
1713
|
-
}
|
|
1714
|
-
}
|
|
1715
|
-
process.exit(1);
|
|
1716
|
-
});
|
|
1717
|
-
process.on("unhandledRejection", (reason) => {
|
|
1718
|
-
const message = reason instanceof Error ? reason.message : String(reason);
|
|
1719
|
-
console.error("QueQue: unhandled rejection:", message);
|
|
1720
|
-
const resultFile = process.env.QQ_RESULT_FILE;
|
|
1721
|
-
if (resultFile && QQ_RESULT_FILE_PATTERN.test(resultFile)) {
|
|
1722
|
-
try {
|
|
1723
|
-
fs2.writeFileSync(resultFile, '{"kind":"cancel"}\n');
|
|
1724
|
-
} catch {
|
|
1725
|
-
}
|
|
1726
|
-
}
|
|
1727
|
-
process.exit(1);
|
|
1728
|
-
});
|
|
1729
|
-
async function main(argv = process.argv.slice(2)) {
|
|
1730
|
-
bootstrapBuiltins();
|
|
1731
|
-
const cli = cac("qq");
|
|
1732
|
-
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) => {
|
|
1733
|
-
const requestFile = requiredOption("--request-file", options.requestFile);
|
|
1734
|
-
const resultFile = requiredOption("--result-file", options.resultFile);
|
|
1735
|
-
await clientCommand({ requestFile, resultFile, resultMode: options.resultMode });
|
|
1736
|
-
});
|
|
1737
|
-
cli.command(
|
|
1738
|
-
"init <shell>",
|
|
1739
|
-
"Print a source line for shell integration (append to ~/.zshrc with `qq init zsh >> ~/.zshrc`)"
|
|
1740
|
-
).action((shell) => {
|
|
1741
|
-
initCommand(shell);
|
|
1742
|
-
});
|
|
1743
|
-
cli.command("daemon").option("--socket <path>", "Unix socket path for the daemon").option("--ensure", "Ensure the daemon is running before returning").action(async (options) => {
|
|
1744
|
-
await daemonCommand(options);
|
|
1745
|
-
});
|
|
1746
|
-
cli.help();
|
|
1747
|
-
cli.parse(["node", "qq", ...argv], { run: false });
|
|
1748
|
-
await cli.runMatchedCommand();
|
|
1749
|
-
}
|
|
1750
|
-
var isDirectRun = process.argv[1] ? import.meta.url === new URL(`file://${fs2.realpathSync(process.argv[1])}`).href : false;
|
|
1751
|
-
if (isDirectRun) {
|
|
1752
|
-
void main().catch((error) => {
|
|
1753
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1754
|
-
console.error(message);
|
|
1755
|
-
process.exitCode = 1;
|
|
1756
|
-
});
|
|
45
|
+
process.exitCode = 1;
|
|
1757
46
|
}
|
|
1758
|
-
export {
|
|
1759
|
-
main
|
|
1760
|
-
};
|
|
1761
47
|
//# sourceMappingURL=main.js.map
|