ask-pro 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/.codex-plugin/plugin.json +30 -0
  2. package/LICENSE +21 -0
  3. package/README.md +231 -0
  4. package/assets/ask-pro_logo.png +0 -0
  5. package/dist/bin/ask-pro-cli.js +507 -0
  6. package/dist/scripts/run-cli.js +27 -0
  7. package/dist/src/ask-pro/atomicWrite.js +26 -0
  8. package/dist/src/ask-pro/browserRunner.js +796 -0
  9. package/dist/src/ask-pro/responseZip.js +349 -0
  10. package/dist/src/ask-pro/session.js +662 -0
  11. package/dist/src/ask-pro/sessionControllerLease.js +64 -0
  12. package/dist/src/ask-pro/toon.js +26 -0
  13. package/dist/src/ask-pro/zip.js +85 -0
  14. package/dist/src/browser/actions/assistantResponse.js +1245 -0
  15. package/dist/src/browser/actions/attachmentDataTransfer.js +140 -0
  16. package/dist/src/browser/actions/attachments.js +1720 -0
  17. package/dist/src/browser/actions/composerSendReadiness.js +369 -0
  18. package/dist/src/browser/actions/domEvents.js +31 -0
  19. package/dist/src/browser/actions/inputGuard.js +52 -0
  20. package/dist/src/browser/actions/modelPickerDom.js +68 -0
  21. package/dist/src/browser/actions/modelSelection.js +576 -0
  22. package/dist/src/browser/actions/navigation.js +510 -0
  23. package/dist/src/browser/actions/promptComposer.js +824 -0
  24. package/dist/src/browser/actions/remoteFileTransfer.js +37 -0
  25. package/dist/src/browser/actions/thinkingStatus.js +408 -0
  26. package/dist/src/browser/actions/thinkingTime.js +635 -0
  27. package/dist/src/browser/actions/windowState.js +47 -0
  28. package/dist/src/browser/attachRunning.js +31 -0
  29. package/dist/src/browser/chatgptModelCatalog.js +321 -0
  30. package/dist/src/browser/chromeLifecycle.js +807 -0
  31. package/dist/src/browser/config.js +110 -0
  32. package/dist/src/browser/constants.js +85 -0
  33. package/dist/src/browser/cookies.js +191 -0
  34. package/dist/src/browser/detect.js +337 -0
  35. package/dist/src/browser/domDebug.js +72 -0
  36. package/dist/src/browser/errors.js +20 -0
  37. package/dist/src/browser/format.js +16 -0
  38. package/dist/src/browser/index.js +2631 -0
  39. package/dist/src/browser/language.js +97 -0
  40. package/dist/src/browser/liveTabs.js +434 -0
  41. package/dist/src/browser/modelStrategy.js +13 -0
  42. package/dist/src/browser/pageActions.js +5 -0
  43. package/dist/src/browser/profilePaths.js +282 -0
  44. package/dist/src/browser/profileState.js +413 -0
  45. package/dist/src/browser/providerDomFlow.js +17 -0
  46. package/dist/src/browser/providers/chatgptDomProvider.js +50 -0
  47. package/dist/src/browser/reattach.js +534 -0
  48. package/dist/src/browser/reattachHelpers.js +387 -0
  49. package/dist/src/browser/utils.js +122 -0
  50. package/dist/src/browserMode.js +1 -0
  51. package/dist/src/version.js +39 -0
  52. package/package.json +114 -0
  53. package/scripts/refresh-local-plugin.mjs +179 -0
  54. package/scripts/refresh-local-plugin.ps1 +93 -0
  55. package/skills/ask-pro/SKILL.md +181 -0
@@ -0,0 +1,662 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import fg from "fast-glob";
5
+ import { atomicWriteFile } from "./atomicWrite.js";
6
+ import { createStoredZip } from "./zip.js";
7
+ const DEFAULT_EXCLUDES = [
8
+ ".ask-pro/**",
9
+ ".env",
10
+ ".env.*",
11
+ "**/*.pem",
12
+ "**/*.key",
13
+ "node_modules/**",
14
+ "dist/**",
15
+ "build/**",
16
+ ".next/**",
17
+ "target/**",
18
+ "vendor/**",
19
+ ".git/**",
20
+ ];
21
+ const SESSION_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9-]{0,127}$/;
22
+ const SESSION_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
23
+ export async function createAskProSession({ cwd, question, filePatterns, dryRun, artifacts = false, }) {
24
+ const trimmedQuestion = question.trim();
25
+ if (!trimmedQuestion) {
26
+ throw new Error("ask-pro requires a question.");
27
+ }
28
+ const { sessionId, sessionDir } = await createSessionDirectory(cwd, trimmedQuestion);
29
+ const collected = await collectContextFiles({ cwd, filePatterns });
30
+ const redactionFindings = [];
31
+ const redactedFiles = await Promise.all(collected.includedFiles.map(async (file) => {
32
+ const absolute = path.join(cwd, file.path);
33
+ const raw = await fs.readFile(absolute, "utf8");
34
+ const redacted = redactSecrets(raw, file.path, redactionFindings);
35
+ return { path: file.path, content: redacted };
36
+ }));
37
+ const manifest = {
38
+ schemaVersion: 1,
39
+ sessionId,
40
+ question: trimmedQuestion,
41
+ includedFiles: collected.includedFiles,
42
+ excludedFiles: collected.excludedFiles,
43
+ redaction: {
44
+ mode: "best_effort",
45
+ findings: redactionFindings,
46
+ },
47
+ };
48
+ const now = new Date().toISOString();
49
+ const status = {
50
+ schemaVersion: 1,
51
+ sessionId,
52
+ status: dryRun ? "DRY_RUN_COMPLETE" : "READY_TO_SUBMIT",
53
+ createdAt: now,
54
+ updatedAt: now,
55
+ resumeCommand: `ask-pro --resume ${sessionId}`,
56
+ harvestCommand: `ask-pro --harvest ${sessionId}`,
57
+ dryRun,
58
+ artifacts,
59
+ };
60
+ const submittedPrompt = renderSubmittedPrompt(question, artifacts);
61
+ const manifestMarkdown = renderManifestMarkdown(manifest);
62
+ const browserMetadata = {
63
+ schemaVersion: 1,
64
+ status: dryRun ? "not_started" : "pending",
65
+ notes: dryRun
66
+ ? ["Dry run only; no browser was opened."]
67
+ : ["Browser submission is pending ask-pro runner wiring."],
68
+ };
69
+ const answer = dryRun
70
+ ? "# Dry Run\n\nNo browser submission was performed.\n"
71
+ : "# Pending\n\nBrowser submission is not wired in this slice.\n";
72
+ await Promise.all([
73
+ fs.writeFile(path.join(sessionDir, "PROMPT.md"), submittedPrompt, "utf8"),
74
+ fs.writeFile(path.join(sessionDir, "MANIFEST.md"), manifestMarkdown, "utf8"),
75
+ fs.writeFile(path.join(sessionDir, "MANIFEST.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8"),
76
+ fs.writeFile(path.join(sessionDir, "status.json"), `${JSON.stringify(status, null, 2)}\n`, "utf8"),
77
+ fs.writeFile(path.join(sessionDir, "browser.json"), `${JSON.stringify(browserMetadata, null, 2)}\n`, "utf8"),
78
+ fs.writeFile(path.join(sessionDir, "ANSWER.md"), answer, "utf8"),
79
+ fs.writeFile(path.join(sessionDir, "log.txt"), renderLog(status, manifest), "utf8"),
80
+ ]);
81
+ const zipEntries = [
82
+ { name: "MANIFEST.md", data: manifestMarkdown },
83
+ ...redactedFiles.map((file) => ({
84
+ name: `context/${file.path.replace(/\\/g, "/")}`,
85
+ data: file.content,
86
+ })),
87
+ ];
88
+ await fs.writeFile(path.join(sessionDir, "CONTEXT.zip"), createStoredZip(zipEntries));
89
+ return { id: sessionId, dir: sessionDir, status, manifest };
90
+ }
91
+ export function getAskProSessionPaths(cwd, sessionId) {
92
+ const dir = resolveAskProSessionDir(cwd, sessionId);
93
+ return {
94
+ dir,
95
+ prompt: path.join(dir, "PROMPT.md"),
96
+ manifestMarkdown: path.join(dir, "MANIFEST.md"),
97
+ manifestJson: path.join(dir, "MANIFEST.json"),
98
+ contextZip: path.join(dir, "CONTEXT.zip"),
99
+ answer: path.join(dir, "ANSWER.md"),
100
+ browser: path.join(dir, "browser.json"),
101
+ status: path.join(dir, "status.json"),
102
+ log: path.join(dir, "log.txt"),
103
+ };
104
+ }
105
+ export async function pruneExpiredAskProSessions({ cwd, now = Date.now(), }) {
106
+ const root = getAskProSessionsRoot(cwd);
107
+ let entries;
108
+ try {
109
+ entries = await fs.readdir(root, { withFileTypes: true });
110
+ }
111
+ catch (error) {
112
+ if (isNodeError(error) && error.code === "ENOENT")
113
+ return 0;
114
+ throw error;
115
+ }
116
+ let removed = 0;
117
+ for (const entry of entries) {
118
+ if (!entry.isDirectory() || !isValidAskProSessionId(entry.name))
119
+ continue;
120
+ const sessionDir = resolveAskProSessionDir(cwd, entry.name);
121
+ let createdAtMs;
122
+ try {
123
+ createdAtMs = await readSessionRetentionTimestamp(sessionDir);
124
+ }
125
+ catch (error) {
126
+ if (isNodeError(error) && error.code === "ENOENT")
127
+ continue;
128
+ throw error;
129
+ }
130
+ if (createdAtMs > now - SESSION_RETENTION_MS)
131
+ continue;
132
+ try {
133
+ await fs.rm(sessionDir, {
134
+ recursive: true,
135
+ force: true,
136
+ maxRetries: 3,
137
+ retryDelay: 100,
138
+ });
139
+ removed += 1;
140
+ }
141
+ catch (error) {
142
+ if (isNodeError(error) &&
143
+ ["ENOENT", "EBUSY", "EACCES", "EPERM", "ENOTEMPTY"].includes(error.code ?? "")) {
144
+ continue;
145
+ }
146
+ throw error;
147
+ }
148
+ }
149
+ return removed;
150
+ }
151
+ export async function updateAskProStatus({ cwd, sessionId, status, reason, temporary, }) {
152
+ const paths = getAskProSessionPaths(cwd, sessionId);
153
+ const current = JSON.parse(await fs.readFile(paths.status, "utf8"));
154
+ const { reason: _currentReason, ...currentWithoutReason } = current;
155
+ const next = {
156
+ ...currentWithoutReason,
157
+ status,
158
+ updatedAt: new Date().toISOString(),
159
+ ...(reason ? { reason } : {}),
160
+ ...(temporary !== undefined ? { temporary } : {}),
161
+ };
162
+ await atomicWriteFile(paths.status, `${JSON.stringify(next, null, 2)}\n`);
163
+ await appendAskProLog(cwd, sessionId, `status=${status}${reason ? ` reason=${reason}` : ""}`);
164
+ return next;
165
+ }
166
+ export async function updateAskProResumeCommand({ cwd, sessionId, resumeCommand, harvestCommand, temporary, }) {
167
+ const paths = getAskProSessionPaths(cwd, sessionId);
168
+ const current = JSON.parse(await fs.readFile(paths.status, "utf8"));
169
+ const next = {
170
+ ...current,
171
+ resumeCommand,
172
+ harvestCommand: harvestCommand ?? current.harvestCommand,
173
+ ...(temporary !== undefined ? { temporary } : {}),
174
+ updatedAt: new Date().toISOString(),
175
+ };
176
+ await atomicWriteFile(paths.status, `${JSON.stringify(next, null, 2)}\n`);
177
+ return next;
178
+ }
179
+ export async function writeAskProAnswer({ cwd, sessionId, answer, }) {
180
+ const paths = getAskProSessionPaths(cwd, sessionId);
181
+ await atomicWriteFile(paths.answer, answer.endsWith("\n") ? answer : `${answer}\n`);
182
+ }
183
+ export async function writeAskProBrowserMetadata({ cwd, sessionId, metadata, }) {
184
+ const paths = getAskProSessionPaths(cwd, sessionId);
185
+ await atomicWriteFile(paths.browser, `${JSON.stringify(metadata, null, 2)}\n`);
186
+ }
187
+ export async function appendAskProLog(cwd, sessionId, message) {
188
+ const paths = getAskProSessionPaths(cwd, sessionId);
189
+ const line = `${new Date().toISOString()} ${redactSecretsForLog(message)}\n`;
190
+ await fs.appendFile(paths.log, line, "utf8");
191
+ }
192
+ export async function readAskProStatus({ cwd, sessionId, }) {
193
+ const id = sessionId ?? (await findLatestSessionId(cwd));
194
+ const paths = getAskProSessionPaths(cwd, id);
195
+ const raw = await fs.readFile(paths.status, "utf8");
196
+ return { dir: paths.dir, status: JSON.parse(raw) };
197
+ }
198
+ export async function readAskProAnswer({ cwd, sessionId, }) {
199
+ const { status, dir } = await readAskProStatus({ cwd, sessionId });
200
+ const answer = await fs.readFile(path.join(dir, "ANSWER.md"), "utf8");
201
+ return { sessionId: status.sessionId, answer };
202
+ }
203
+ export async function readAskProPrompt({ cwd, sessionId, }) {
204
+ const paths = getAskProSessionPaths(cwd, sessionId);
205
+ return fs.readFile(paths.prompt, "utf8");
206
+ }
207
+ async function findLatestSessionId(cwd) {
208
+ const root = getAskProSessionsRoot(cwd);
209
+ const entries = await fs.readdir(root, { withFileTypes: true });
210
+ const sessions = (await Promise.all(entries
211
+ .filter((entry) => entry.isDirectory() && isValidAskProSessionId(entry.name))
212
+ .map((entry) => readSessionCreatedAt(cwd, entry.name)))).filter((session) => session !== undefined);
213
+ const latest = sessions.sort((left, right) => left.createdAtMs - right.createdAtMs ||
214
+ left.tiebreakerMs - right.tiebreakerMs ||
215
+ left.sessionId.localeCompare(right.sessionId))[sessions.length - 1]?.sessionId;
216
+ if (!latest) {
217
+ throw new Error("No ask-pro sessions found.");
218
+ }
219
+ return latest;
220
+ }
221
+ async function readSessionRetentionTimestamp(sessionDir) {
222
+ try {
223
+ const status = JSON.parse(await fs.readFile(path.join(sessionDir, "status.json"), "utf8"));
224
+ const createdAt = typeof status.createdAt === "string" ? Date.parse(status.createdAt) : NaN;
225
+ if (Number.isFinite(createdAt))
226
+ return createdAt;
227
+ }
228
+ catch {
229
+ // Fall back to the directory timestamp for damaged legacy sessions.
230
+ }
231
+ return (await fs.stat(sessionDir)).mtimeMs;
232
+ }
233
+ async function readSessionCreatedAt(cwd, sessionId) {
234
+ const statusPath = path.join(resolveAskProSessionDir(cwd, sessionId), "status.json");
235
+ try {
236
+ const [raw, stat] = await Promise.all([fs.readFile(statusPath, "utf8"), fs.stat(statusPath)]);
237
+ const status = JSON.parse(raw);
238
+ const createdAt = typeof status.createdAt === "string" ? Date.parse(status.createdAt) : NaN;
239
+ const createdAtMs = Number.isFinite(createdAt) ? createdAt : 0;
240
+ return {
241
+ sessionId,
242
+ createdAtMs,
243
+ tiebreakerMs: stat.birthtimeMs || stat.ctimeMs || stat.mtimeMs,
244
+ };
245
+ }
246
+ catch {
247
+ return undefined;
248
+ }
249
+ }
250
+ async function createSessionDirectory(cwd, question) {
251
+ const root = getAskProSessionsRoot(cwd);
252
+ await fs.mkdir(root, { recursive: true });
253
+ for (let attempt = 0; attempt < 8; attempt += 1) {
254
+ const sessionId = buildSessionId(question);
255
+ const sessionDir = resolveAskProSessionDir(cwd, sessionId);
256
+ try {
257
+ await fs.mkdir(sessionDir);
258
+ return { sessionId, sessionDir };
259
+ }
260
+ catch (error) {
261
+ if (isNodeError(error) && error.code === "EEXIST") {
262
+ continue;
263
+ }
264
+ throw error;
265
+ }
266
+ }
267
+ throw new Error("Could not allocate a unique ask-pro session id.");
268
+ }
269
+ function getAskProSessionsRoot(cwd) {
270
+ return path.resolve(cwd, ".ask-pro", "sessions");
271
+ }
272
+ function resolveAskProSessionDir(cwd, sessionId) {
273
+ validateAskProSessionId(sessionId);
274
+ const root = getAskProSessionsRoot(cwd);
275
+ const dir = path.resolve(root, sessionId);
276
+ const relative = path.relative(root, dir);
277
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
278
+ throw new Error(`Invalid ask-pro session id: ${sessionId}`);
279
+ }
280
+ return dir;
281
+ }
282
+ function validateAskProSessionId(sessionId) {
283
+ if (!isValidAskProSessionId(sessionId)) {
284
+ throw new Error(`Invalid ask-pro session id: ${sessionId}`);
285
+ }
286
+ }
287
+ function isValidAskProSessionId(sessionId) {
288
+ return SESSION_ID_PATTERN.test(sessionId);
289
+ }
290
+ function isNodeError(error) {
291
+ return error instanceof Error && "code" in error;
292
+ }
293
+ async function collectContextFiles({ cwd, filePatterns, }) {
294
+ const patterns = await normalizeFilePatterns(cwd, filePatterns);
295
+ const matched = patterns.length > 0
296
+ ? await fg(patterns, {
297
+ cwd,
298
+ onlyFiles: true,
299
+ dot: true,
300
+ unique: true,
301
+ ignore: DEFAULT_EXCLUDES,
302
+ })
303
+ : [];
304
+ const realCwd = await realpathIfExists(cwd);
305
+ const includedFiles = await Promise.all(matched.sort().map(async (entry) => ({
306
+ path: await normalizeMatchedFilePath(cwd, realCwd, entry),
307
+ reason: "Matched by --files pattern.",
308
+ })));
309
+ const excludedFiles = DEFAULT_EXCLUDES.map((entry) => ({
310
+ path: entry,
311
+ reason: "Default safety exclude.",
312
+ }));
313
+ return { includedFiles, excludedFiles };
314
+ }
315
+ async function normalizeFilePatterns(cwd, filePatterns) {
316
+ return Promise.all(filePatterns.map((pattern) => normalizeFilePattern(cwd, pattern)));
317
+ }
318
+ async function normalizeFilePattern(cwd, pattern) {
319
+ const normalized = pattern.replace(/\\/g, "/");
320
+ const asPath = path.resolve(cwd, normalized);
321
+ const realCwd = await realpathIfExists(cwd);
322
+ await assertFilePatternPrefixInsideCwd(cwd, realCwd, pattern);
323
+ if (path.isAbsolute(pattern)) {
324
+ const realTarget = await realpathIfExists(asPath);
325
+ const realRelative = path.relative(realCwd, realTarget);
326
+ if (isOutsidePath(realRelative)) {
327
+ throw new Error(`--files path must be inside the project cwd: ${pattern}`);
328
+ }
329
+ const relative = path.relative(cwd, asPath);
330
+ if (isOutsidePath(relative)) {
331
+ throw new Error(`--files path must be inside the project cwd: ${pattern}`);
332
+ }
333
+ return expandDirectoryPattern(asPath, normalizeManifestPath(relative) || ".");
334
+ }
335
+ const realTarget = await realpathIfExists(asPath);
336
+ const realRelative = path.relative(realCwd, realTarget);
337
+ if ((await pathExists(asPath)) && isOutsidePath(realRelative)) {
338
+ throw new Error(`--files path must be inside the project cwd: ${pattern}`);
339
+ }
340
+ return expandDirectoryPattern(asPath, normalized);
341
+ }
342
+ async function assertFilePatternPrefixInsideCwd(cwd, realCwd, pattern) {
343
+ await Promise.all(expandBraceAlternatives(pattern).map((expandedPattern) => assertExpandedFilePatternInsideCwd(cwd, realCwd, expandedPattern, pattern)));
344
+ }
345
+ async function assertExpandedFilePatternInsideCwd(cwd, realCwd, expandedPattern, originalPattern) {
346
+ assertNoRootedGlobAlternative(expandedPattern, originalPattern);
347
+ assertNoUnsafeExtglobBody(expandedPattern, originalPattern);
348
+ const { prefix, globTail } = splitPatternAtFirstGlob(expandedPattern);
349
+ const absolutePrefix = path.resolve(cwd, prefix || ".");
350
+ const lexicalRelative = path.relative(path.resolve(cwd), absolutePrefix);
351
+ if (isOutsidePath(lexicalRelative)) {
352
+ throw new Error(`--files path must be inside the project cwd: ${originalPattern}`);
353
+ }
354
+ assertGlobTailInsideCwd(lexicalRelative, globTail, originalPattern);
355
+ if (!(await pathExists(absolutePrefix))) {
356
+ return;
357
+ }
358
+ const realPrefix = await fs.realpath(absolutePrefix);
359
+ const realRelative = path.relative(realCwd, realPrefix);
360
+ if (isOutsidePath(realRelative)) {
361
+ throw new Error(`--files path must be inside the project cwd: ${originalPattern}`);
362
+ }
363
+ }
364
+ function expandBraceAlternatives(pattern) {
365
+ const results = [];
366
+ const visit = (source) => {
367
+ const brace = findExpandableBrace(source);
368
+ if (!brace) {
369
+ results.push(source);
370
+ return;
371
+ }
372
+ for (const alternative of brace.alternatives) {
373
+ if (results.length >= 64) {
374
+ throw new Error(`--files pattern has too many brace alternatives: ${pattern}`);
375
+ }
376
+ visit(`${source.slice(0, brace.start)}${alternative}${source.slice(brace.end + 1)}`);
377
+ }
378
+ };
379
+ visit(pattern.replace(/\\/g, "/"));
380
+ return results;
381
+ }
382
+ function findExpandableBrace(pattern) {
383
+ for (let start = 0; start < pattern.length; start += 1) {
384
+ if (pattern[start] !== "{") {
385
+ continue;
386
+ }
387
+ let depth = 0;
388
+ for (let end = start; end < pattern.length; end += 1) {
389
+ if (pattern[end] === "{") {
390
+ depth += 1;
391
+ }
392
+ else if (pattern[end] === "}") {
393
+ depth -= 1;
394
+ }
395
+ if (depth === 0) {
396
+ const alternatives = splitBraceAlternatives(pattern.slice(start + 1, end));
397
+ if (alternatives.length > 1) {
398
+ return { start, end, alternatives };
399
+ }
400
+ start = end;
401
+ break;
402
+ }
403
+ }
404
+ }
405
+ return undefined;
406
+ }
407
+ function splitBraceAlternatives(body) {
408
+ const alternatives = [];
409
+ let depth = 0;
410
+ let segmentStart = 0;
411
+ for (let index = 0; index < body.length; index += 1) {
412
+ if (body[index] === "{") {
413
+ depth += 1;
414
+ }
415
+ else if (body[index] === "}") {
416
+ depth -= 1;
417
+ }
418
+ else if (body[index] === "," && depth === 0) {
419
+ alternatives.push(body.slice(segmentStart, index));
420
+ segmentStart = index + 1;
421
+ }
422
+ }
423
+ alternatives.push(body.slice(segmentStart));
424
+ return alternatives.length === 1 ? [] : alternatives;
425
+ }
426
+ function splitPatternAtFirstGlob(pattern) {
427
+ const normalized = pattern.replace(/\\/g, "/");
428
+ const segments = normalized.split("/");
429
+ const firstGlobSegment = segments.findIndex(hasGlobSyntax);
430
+ return firstGlobSegment === -1
431
+ ? { prefix: normalized, globTail: [] }
432
+ : {
433
+ prefix: segments.slice(0, firstGlobSegment).join("/"),
434
+ globTail: segments.slice(firstGlobSegment),
435
+ };
436
+ }
437
+ function assertGlobTailInsideCwd(lexicalRelativePrefix, globTail, pattern) {
438
+ let depth = lexicalRelativePrefix === ""
439
+ ? 0
440
+ : lexicalRelativePrefix.replace(/\\/g, "/").split("/").filter(Boolean).length;
441
+ for (const segment of globTail) {
442
+ if (segment === "" || segment === ".") {
443
+ continue;
444
+ }
445
+ if (globSegmentCanExpandToParent(segment)) {
446
+ depth -= 1;
447
+ }
448
+ else if (hasGlobSyntax(segment)) {
449
+ depth += globSegmentMinimumDepth(segment);
450
+ }
451
+ else if (segment === "..") {
452
+ depth -= 1;
453
+ }
454
+ else {
455
+ depth += 1;
456
+ }
457
+ if (depth < 0) {
458
+ throw new Error(`--files path must be inside the project cwd: ${pattern}`);
459
+ }
460
+ }
461
+ }
462
+ function hasGlobSyntax(segment) {
463
+ return /[*?[\]{}]|[!+@]\(/.test(segment);
464
+ }
465
+ function globSegmentCanExpandToParent(segment) {
466
+ return (hasGlobSyntax(segment) &&
467
+ (/(^|[,{(|])\.\.($|[,}|)])/.test(segment) ||
468
+ (hasNestedExtglob(segment) && segment.includes(".")) ||
469
+ (segment.includes("..") && segmentContainsEmptyCapableExtglob(segment)) ||
470
+ segment.replaceAll(emptyCapableExtglobPattern, "") === ".."));
471
+ }
472
+ function globSegmentMinimumDepth(segment) {
473
+ if (segment === "**" || extglobSegmentCanBeEmpty(segment)) {
474
+ return 0;
475
+ }
476
+ return 1;
477
+ }
478
+ function extglobSegmentCanBeEmpty(segment) {
479
+ const body = segment.match(/^([!*+@?])\((.*)\)$/)?.[2];
480
+ return (body !== undefined &&
481
+ (hasNestedExtglob(segment) ||
482
+ segment.startsWith("!(") ||
483
+ segment.startsWith("?(") ||
484
+ segment.startsWith("*(") ||
485
+ body.includes("!(") ||
486
+ body.includes("?(") ||
487
+ body.includes("*(") ||
488
+ body.includes("(|") ||
489
+ body.includes("|)") ||
490
+ body.split("|").includes("")));
491
+ }
492
+ const emptyCapableExtglobPattern = /[!?*]\([^)]*\)|[+@]\([^)]*(?:\|\)|\(\||\|\|)[^)]*\)/g;
493
+ function hasNestedExtglob(segment) {
494
+ return /[!+@?*]\([^)]*[!+@?*]\(/.test(segment);
495
+ }
496
+ function segmentContainsEmptyCapableExtglob(segment) {
497
+ emptyCapableExtglobPattern.lastIndex = 0;
498
+ return emptyCapableExtglobPattern.test(segment);
499
+ }
500
+ function assertNoRootedGlobAlternative(pattern, originalPattern) {
501
+ if (/[,(|](?:\/|[A-Za-z]:\/)/.test(pattern)) {
502
+ throw new Error(`--files path must be inside the project cwd: ${originalPattern}`);
503
+ }
504
+ }
505
+ function assertNoUnsafeExtglobBody(pattern, originalPattern) {
506
+ const normalized = pattern.replace(/\\/g, "/");
507
+ for (let index = 0; index < normalized.length - 1; index += 1) {
508
+ if (!isExtglobOperator(normalized[index]) || normalized[index + 1] !== "(") {
509
+ continue;
510
+ }
511
+ const end = findMatchingParen(normalized, index + 1);
512
+ if (end === undefined) {
513
+ continue;
514
+ }
515
+ const body = normalized.slice(index + 2, end);
516
+ if (body.includes("/") || body.includes("..")) {
517
+ throw new Error(`--files path must be inside the project cwd: ${originalPattern}`);
518
+ }
519
+ index = end;
520
+ }
521
+ }
522
+ function findMatchingParen(pattern, openIndex) {
523
+ let depth = 0;
524
+ for (let index = openIndex; index < pattern.length; index += 1) {
525
+ if (pattern[index] === "(") {
526
+ depth += 1;
527
+ }
528
+ else if (pattern[index] === ")") {
529
+ depth -= 1;
530
+ if (depth === 0) {
531
+ return index;
532
+ }
533
+ }
534
+ }
535
+ return undefined;
536
+ }
537
+ function isExtglobOperator(char) {
538
+ return char === "!" || char === "+" || char === "@" || char === "?" || char === "*";
539
+ }
540
+ async function normalizeMatchedFilePath(cwd, realCwd, entry) {
541
+ const absolute = path.resolve(cwd, entry);
542
+ const realEntry = await fs.realpath(absolute);
543
+ const realRelative = path.relative(realCwd, realEntry);
544
+ if (isOutsidePath(realRelative)) {
545
+ throw new Error(`--files path must be inside the project cwd: ${entry}`);
546
+ }
547
+ return normalizeManifestPath(realRelative);
548
+ }
549
+ function isOutsidePath(relativePath) {
550
+ return (relativePath === ".." ||
551
+ relativePath.startsWith(`..${path.sep}`) ||
552
+ path.isAbsolute(relativePath));
553
+ }
554
+ async function realpathIfExists(filePath) {
555
+ try {
556
+ return await fs.realpath(filePath);
557
+ }
558
+ catch {
559
+ return path.resolve(filePath);
560
+ }
561
+ }
562
+ async function pathExists(filePath) {
563
+ try {
564
+ await fs.access(filePath);
565
+ return true;
566
+ }
567
+ catch {
568
+ return false;
569
+ }
570
+ }
571
+ async function expandDirectoryPattern(absolutePath, pattern) {
572
+ try {
573
+ const stat = await fs.stat(absolutePath);
574
+ if (stat.isDirectory()) {
575
+ return `${pattern.replace(/\/+$/g, "")}/**`;
576
+ }
577
+ }
578
+ catch {
579
+ // Missing paths may be globs; let fast-glob handle them.
580
+ }
581
+ return pattern;
582
+ }
583
+ function buildSessionId(question, now = new Date()) {
584
+ const [date, time = ""] = now.toISOString().split("T");
585
+ const compactTime = time.replace(/:/g, "").replace(/\.\d{3}Z$/, "");
586
+ return `${date}T${compactTime}-${slugify(question)}-${randomBytes(4).toString("hex")}`;
587
+ }
588
+ function slugify(question) {
589
+ const slug = question
590
+ .toLowerCase()
591
+ .replace(/[^a-z0-9]+/g, "-")
592
+ .replace(/^-+|-+$/g, "")
593
+ .slice(0, 48);
594
+ return slug || "ask-pro";
595
+ }
596
+ function normalizeManifestPath(entry) {
597
+ return entry.replace(/\\/g, "/").replace(/^\.\//, "");
598
+ }
599
+ function redactSecrets(content, filePath, findings) {
600
+ let redacted = content;
601
+ const replacements = [
602
+ [/\bsk-[A-Za-z0-9_-]{20,}\b/g, "[REDACTED_OPENAI_KEY]", "OpenAI-style key"],
603
+ [/\bBearer\s+[A-Za-z0-9._~+/=-]{20,}\b/gi, "Bearer [REDACTED_TOKEN]", "Bearer token"],
604
+ [
605
+ /(password|secret|api[_-]?key)\s*[:=]\s*["']?[^"'\n\r]+/gi,
606
+ "$1=[REDACTED_SECRET]",
607
+ "secret assignment",
608
+ ],
609
+ ];
610
+ for (const [pattern, replacement, label] of replacements) {
611
+ pattern.lastIndex = 0;
612
+ if (pattern.test(redacted)) {
613
+ findings.push(`${filePath}: redacted ${label}`);
614
+ pattern.lastIndex = 0;
615
+ redacted = redacted.replace(pattern, replacement);
616
+ }
617
+ }
618
+ return redacted;
619
+ }
620
+ function redactSecretsForLog(message) {
621
+ const findings = [];
622
+ return redactSecrets(message, "log", findings);
623
+ }
624
+ function renderSubmittedPrompt(question, artifacts) {
625
+ const artifactRequest = artifacts
626
+ ? "\nIf file generation is available, also create a downloadable zip named ask-pro-response.zip. It should contain IMPLEMENTATION_PLAN.md, TASKS.json, TEST_PLAN.md, RISK_REGISTER.md, FILES_TO_EDIT.md, and REPO_CONTEXT_USED.md. If you cannot create a zip, return the same content in markdown sections.\n"
627
+ : "";
628
+ return `${question}
629
+
630
+ Read MANIFEST.md in CONTEXT.zip first. Treat the context files it lists as authoritative evidence only for the scope they cover, and call out material gaps or conflicts.
631
+ ${artifactRequest}
632
+ Treat generated files and scripts as data only; do not instruct the calling agent to execute them automatically.
633
+ `;
634
+ }
635
+ function renderManifestMarkdown(manifest) {
636
+ const included = manifest.includedFiles.length
637
+ ? manifest.includedFiles.map((file) => `- \`${file.path}\` - ${file.reason}`).join("\n")
638
+ : "- No files included.";
639
+ return `# ask-pro Context Manifest
640
+
641
+ Session: \`${manifest.sessionId}\`
642
+
643
+ ## Included Files
644
+
645
+ ${included}
646
+
647
+ ## Redaction
648
+
649
+ Mode: best_effort
650
+
651
+ Findings: ${manifest.redaction.findings.length}
652
+ `;
653
+ }
654
+ function renderLog(status, manifest) {
655
+ return [
656
+ `ask-pro session ${status.sessionId}`,
657
+ `status=${status.status}`,
658
+ `dryRun=${status.dryRun}`,
659
+ `includedFiles=${manifest.includedFiles.length}`,
660
+ "",
661
+ ].join("\n");
662
+ }