@yagni-app/code 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -0
- package/dist/cli.js +231 -6
- package/dist/crashReport.d.ts +8 -0
- package/dist/crashReport.js +13 -1
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +33 -0
- package/dist/extension/askAdvisorTool.d.ts +7 -0
- package/dist/extension/askAdvisorTool.js +11 -3
- package/dist/extension/askYagniTool.js +2 -0
- package/dist/extension/branding.d.ts +15 -0
- package/dist/extension/branding.js +76 -0
- package/dist/extension/chipEditor.d.ts +22 -1
- package/dist/extension/chipEditor.js +58 -5
- package/dist/extension/condensedTools.d.ts +93 -0
- package/dist/extension/condensedTools.js +392 -0
- package/dist/extension/diffStat.d.ts +62 -0
- package/dist/extension/diffStat.js +158 -0
- package/dist/extension/footer.d.ts +2 -0
- package/dist/extension/footer.js +21 -8
- package/dist/extension/index.d.ts +6 -0
- package/dist/extension/index.js +70 -2
- package/dist/extension/permission/execPolicy.js +47 -0
- package/dist/extension/pipeline/invocation.d.ts +7 -0
- package/dist/extension/pipeline/invocation.js +7 -0
- package/dist/extension/pipeline/personas.js +4 -4
- package/dist/extension/pipeline/runner.d.ts +1 -0
- package/dist/extension/pipeline/runner.js +15 -3
- package/dist/extension/pipeline/sessionWorktree.d.ts +64 -0
- package/dist/extension/pipeline/sessionWorktree.js +225 -0
- package/dist/extension/scratchpad.d.ts +66 -0
- package/dist/extension/scratchpad.js +93 -0
- package/dist/extension/subagents.d.ts +10 -0
- package/dist/extension/subagents.js +18 -4
- package/dist/extension/todos.d.ts +1 -0
- package/dist/extension/todos.js +15 -0
- package/dist/extension/toolRuns.d.ts +92 -0
- package/dist/extension/toolRuns.js +201 -0
- package/dist/extension/webFetchTool.js +2 -0
- package/dist/extension/workingLine.d.ts +49 -0
- package/dist/extension/workingLine.js +116 -0
- package/dist/feedback.d.ts +77 -0
- package/dist/feedback.js +500 -0
- package/dist/goHeadless.d.ts +3 -0
- package/dist/goHeadless.js +13 -0
- package/dist/launch.d.ts +8 -0
- package/dist/launch.js +6 -0
- package/dist/otel.d.ts +150 -0
- package/dist/otel.js +291 -0
- package/dist/outputFormat.d.ts +83 -0
- package/dist/outputFormat.js +207 -0
- package/dist/paths.d.ts +10 -0
- package/dist/paths.js +13 -0
- package/dist/worktreeArgs.d.ts +43 -0
- package/dist/worktreeArgs.js +96 -0
- package/package.json +3 -2
package/dist/feedback.js
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `yagni feedback [sessionId]` — file a bug report from the shell (YAG-592).
|
|
3
|
+
*
|
|
4
|
+
* A shortcut to trigger what `/feedback` does inside a session, but from
|
|
5
|
+
* outside the TUI. Two modes:
|
|
6
|
+
*
|
|
7
|
+
* Case A (no session arg): list the 10 most recent sessions for the current
|
|
8
|
+
* cwd, let the user pick, prompt for a description, confirm, submit.
|
|
9
|
+
* Case B (session ID provided): skip the list, go straight to description
|
|
10
|
+
* prompt → confirm → submit.
|
|
11
|
+
*
|
|
12
|
+
* Self-contained: no cross-package imports. The scrub (`scrubSecrets`) and the
|
|
13
|
+
* error-trail reader (`readSessionTrail`) are local copies of the extension's
|
|
14
|
+
* logic — keep in sync with `pi-extension-yagni/src/pipeline/scrubSecrets.ts`
|
|
15
|
+
* and `pi-extension-yagni/src/errorSink.ts`. The backend re-scrubs server-side
|
|
16
|
+
* (`backend/src/yagniCode/feedback.ts`), so the client-side scrub is the first
|
|
17
|
+
* line of defense, not the only one.
|
|
18
|
+
*/
|
|
19
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { createInterface } from "node:readline/promises";
|
|
22
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
23
|
+
import { isatty } from "node:tty";
|
|
24
|
+
import { agentDir, credentialsDir } from "./credentials.js";
|
|
25
|
+
import { scrubSecrets } from "./crashReport.js";
|
|
26
|
+
import { credentialsFromProfile, readActiveProfile } from "./profiles.js";
|
|
27
|
+
const MAX_DESCRIPTION = 512;
|
|
28
|
+
const MAX_TRANSCRIPT_READ_BYTES = 512 * 1024;
|
|
29
|
+
const MAX_TRAIL_BYTES = 64 * 1024;
|
|
30
|
+
const FIRST_MESSAGE_PREVIEW_FALLBACK = 60;
|
|
31
|
+
/** Max chars for the first-message preview, capped by terminal width. */
|
|
32
|
+
function previewMaxWidth(termCols) {
|
|
33
|
+
const cols = termCols ?? process.stdout.columns;
|
|
34
|
+
if (!cols || cols < 40)
|
|
35
|
+
return FIRST_MESSAGE_PREVIEW_FALLBACK;
|
|
36
|
+
// Account for: " " + " N. " (5) + time (18) + dur (6) = ~29 chars of prefix
|
|
37
|
+
const available = cols - 32;
|
|
38
|
+
return Math.max(available, 20);
|
|
39
|
+
}
|
|
40
|
+
const LIST_LIMIT = 10;
|
|
41
|
+
// --- session discovery ---
|
|
42
|
+
/**
|
|
43
|
+
* Encode a cwd into pi's session directory name format:
|
|
44
|
+
* `/Users/foo/bar` → `--Users-foo-bar--`
|
|
45
|
+
* Mirrors pi's `migrations.js`: `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`
|
|
46
|
+
*/
|
|
47
|
+
export function encodeCwd(cwd) {
|
|
48
|
+
return `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Parse the first few lines of a session JSONL to extract metadata.
|
|
52
|
+
* Returns null on any failure (corrupt/empty file).
|
|
53
|
+
*/
|
|
54
|
+
function parseSessionHeader(filePath) {
|
|
55
|
+
try {
|
|
56
|
+
const data = readFileSync(filePath, "utf8");
|
|
57
|
+
const lines = data.split("\n").filter((l) => l.length > 0);
|
|
58
|
+
for (const line of lines) {
|
|
59
|
+
try {
|
|
60
|
+
const obj = JSON.parse(line);
|
|
61
|
+
if (obj.type === "session" && typeof obj.id === "string" && typeof obj.timestamp === "string") {
|
|
62
|
+
return { id: obj.id, timestamp: obj.timestamp };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// skip unparseable lines
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Extract the first user message text from a session JSONL (skips
|
|
77
|
+
* `custom_message` entries — only real `type: "message"` with `role: "user"`).
|
|
78
|
+
*/
|
|
79
|
+
function extractFirstUserMessage(filePath, maxChars) {
|
|
80
|
+
try {
|
|
81
|
+
const data = readFileSync(filePath, "utf8");
|
|
82
|
+
const lines = data.split("\n").filter((l) => l.length > 0);
|
|
83
|
+
for (const line of lines) {
|
|
84
|
+
try {
|
|
85
|
+
const obj = JSON.parse(line);
|
|
86
|
+
if (obj.type === "message" &&
|
|
87
|
+
obj.message?.role === "user" &&
|
|
88
|
+
Array.isArray(obj.message?.content)) {
|
|
89
|
+
const textPart = obj.message.content.find((c) => c.type === "text");
|
|
90
|
+
if (textPart?.text) {
|
|
91
|
+
// Strip skill invocation XML tags (e.g. `<skill name="…" …>`)
|
|
92
|
+
// — they're machinery, not the user's message.
|
|
93
|
+
let text = textPart.text.trim()
|
|
94
|
+
.replace(/<skill\s[^>]*>\s*/g, "")
|
|
95
|
+
.replace(/<\/skill>/g, "")
|
|
96
|
+
.replace(/\n+/g, " ")
|
|
97
|
+
.trim();
|
|
98
|
+
const cap = maxChars ?? FIRST_MESSAGE_PREVIEW_FALLBACK;
|
|
99
|
+
if (text.length > 0) {
|
|
100
|
+
return text.length > cap
|
|
101
|
+
? `${text.slice(0, cap)}…`
|
|
102
|
+
: text;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// skip unparseable lines
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return "";
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return "";
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Get the timestamp of the last non-empty line in a session JSONL.
|
|
119
|
+
* Used to compute session duration.
|
|
120
|
+
*/
|
|
121
|
+
function extractLastTimestamp(filePath) {
|
|
122
|
+
try {
|
|
123
|
+
const data = readFileSync(filePath, "utf8");
|
|
124
|
+
const lines = data.split("\n").filter((l) => l.length > 0);
|
|
125
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
126
|
+
try {
|
|
127
|
+
const obj = JSON.parse(lines[i]);
|
|
128
|
+
if (typeof obj.timestamp === "string")
|
|
129
|
+
return obj.timestamp;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// skip
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* List the most recent sessions for the current cwd.
|
|
143
|
+
* Scans `<agentDir>/sessions/<encoded-cwd>/*.jsonl`, sorted by start time desc.
|
|
144
|
+
*/
|
|
145
|
+
export function listRecentSessions(agentDirPath, cwd, limit = LIST_LIMIT, termCols) {
|
|
146
|
+
const sessionsDir = join(agentDirPath, "sessions", encodeCwd(cwd));
|
|
147
|
+
let files;
|
|
148
|
+
try {
|
|
149
|
+
files = readdirSync(sessionsDir).filter((f) => f.endsWith(".jsonl"));
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
const sessions = [];
|
|
155
|
+
for (const file of files) {
|
|
156
|
+
const filePath = join(sessionsDir, file);
|
|
157
|
+
const header = parseSessionHeader(filePath);
|
|
158
|
+
if (!header)
|
|
159
|
+
continue;
|
|
160
|
+
const firstMessage = extractFirstUserMessage(filePath, previewMaxWidth(termCols));
|
|
161
|
+
const lastTs = extractLastTimestamp(filePath);
|
|
162
|
+
let durationMs = null;
|
|
163
|
+
if (lastTs) {
|
|
164
|
+
const start = Date.parse(header.timestamp);
|
|
165
|
+
const end = Date.parse(lastTs);
|
|
166
|
+
if (!isNaN(start) && !isNaN(end))
|
|
167
|
+
durationMs = end - start;
|
|
168
|
+
}
|
|
169
|
+
sessions.push({
|
|
170
|
+
id: header.id,
|
|
171
|
+
filePath,
|
|
172
|
+
startTime: header.timestamp,
|
|
173
|
+
durationMs,
|
|
174
|
+
firstMessage,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
sessions.sort((a, b) => b.startTime.localeCompare(a.startTime));
|
|
178
|
+
return sessions.slice(0, limit);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Find a session file by UUID across all project dirs.
|
|
182
|
+
*/
|
|
183
|
+
export function findSessionById(agentDirPath, sessionId) {
|
|
184
|
+
const sessionsRoot = join(agentDirPath, "sessions");
|
|
185
|
+
let projectDirs;
|
|
186
|
+
try {
|
|
187
|
+
projectDirs = readdirSync(sessionsRoot, { withFileTypes: true })
|
|
188
|
+
.filter((d) => d.isDirectory())
|
|
189
|
+
.map((d) => join(sessionsRoot, d.name));
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
for (const dir of projectDirs) {
|
|
195
|
+
let files;
|
|
196
|
+
try {
|
|
197
|
+
files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
for (const file of files) {
|
|
203
|
+
if (file.includes(sessionId)) {
|
|
204
|
+
const filePath = join(dir, file);
|
|
205
|
+
const header = parseSessionHeader(filePath);
|
|
206
|
+
if (header)
|
|
207
|
+
return { filePath, startTime: header.timestamp };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
// --- transcript + error trail ---
|
|
214
|
+
/**
|
|
215
|
+
* Read the durable transcript, clamped by byte size. Returns empty on any
|
|
216
|
+
* failure or when too large. Mirrors the extension's `readTranscript`.
|
|
217
|
+
*/
|
|
218
|
+
export function readTranscript(sessionFile) {
|
|
219
|
+
if (!sessionFile)
|
|
220
|
+
return "";
|
|
221
|
+
try {
|
|
222
|
+
const data = readFileSync(sessionFile, "utf8");
|
|
223
|
+
if (Buffer.byteLength(data, "utf8") > MAX_TRANSCRIPT_READ_BYTES)
|
|
224
|
+
return "";
|
|
225
|
+
return data;
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return "";
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Read the session-scoped error trail from today's error sink file.
|
|
233
|
+
* Filters by sessionId and excludes debug-level entries. Scrubs each line.
|
|
234
|
+
* Mirrors the extension's `readSessionTrail` — keep in sync.
|
|
235
|
+
*/
|
|
236
|
+
export function readSessionTrail(sessionId, stateDir, maxBytes = MAX_TRAIL_BYTES) {
|
|
237
|
+
try {
|
|
238
|
+
const dayStamp = new Date().toISOString().slice(0, 10);
|
|
239
|
+
const sinkPath = join(stateDir, "logs", `errors-${dayStamp}.jsonl`);
|
|
240
|
+
const data = readFileSync(sinkPath, "utf8");
|
|
241
|
+
const lines = data
|
|
242
|
+
.split("\n")
|
|
243
|
+
.filter((l) => l.length > 0)
|
|
244
|
+
.filter((l) => {
|
|
245
|
+
try {
|
|
246
|
+
const obj = JSON.parse(l);
|
|
247
|
+
return obj.sessionId === sessionId && obj.level !== "debug";
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
.map((l) => scrubSecrets(l))
|
|
254
|
+
.join("\n");
|
|
255
|
+
return lines.length > maxBytes ? lines.slice(lines.length - maxBytes) : lines;
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return "";
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function buildFeedbackPayload(opts) {
|
|
262
|
+
const transcript = readTranscript(opts.sessionFile);
|
|
263
|
+
const trail = readSessionTrail(opts.sessionId, opts.stateDir);
|
|
264
|
+
return {
|
|
265
|
+
client: "cli",
|
|
266
|
+
clientVersion: opts.env.YAGNI_CODE_VERSION?.trim() || opts.clientVersion || "unknown",
|
|
267
|
+
platform: `${process.platform} ${process.arch}`,
|
|
268
|
+
description: scrubSecrets(opts.description).slice(0, MAX_DESCRIPTION),
|
|
269
|
+
sessionId: opts.sessionId,
|
|
270
|
+
...(transcript ? { transcriptJsonl: scrubSecrets(transcript) } : {}),
|
|
271
|
+
...(trail ? { errorTrailJsonl: trail } : {}),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
async function submitFeedback(payload, baseUrl, token, fetchImpl) {
|
|
275
|
+
try {
|
|
276
|
+
const res = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/api/yagni-code/feedback`, {
|
|
277
|
+
method: "POST",
|
|
278
|
+
headers: {
|
|
279
|
+
"content-type": "application/json",
|
|
280
|
+
authorization: `Bearer ${token}`,
|
|
281
|
+
},
|
|
282
|
+
body: JSON.stringify(payload),
|
|
283
|
+
signal: AbortSignal.timeout(30_000),
|
|
284
|
+
});
|
|
285
|
+
if (res.ok) {
|
|
286
|
+
const body = (await res.json().catch(() => ({})));
|
|
287
|
+
return { ok: true, id: body.id };
|
|
288
|
+
}
|
|
289
|
+
return { ok: false, error: `Server returned ${res.status}` };
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Create a readline-like interface that survives piped stdin (where the
|
|
297
|
+
* real readline's `question` hangs after EOF). For TTY stdin it delegates to
|
|
298
|
+
* the real `readline/promises`; for piped stdin it reads all lines upfront
|
|
299
|
+
* and serves them one-by-one.
|
|
300
|
+
*/
|
|
301
|
+
function makeReadline() {
|
|
302
|
+
if (isatty(0)) {
|
|
303
|
+
const rl = createInterface({ input, output });
|
|
304
|
+
return {
|
|
305
|
+
question: (q) => rl.question(q),
|
|
306
|
+
close: () => rl.close(),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
// Piped stdin: read all available data and serve lines FIFO.
|
|
310
|
+
const lines = [];
|
|
311
|
+
try {
|
|
312
|
+
const data = readFileSync(0, "utf8"); // fd 0 = stdin
|
|
313
|
+
for (const line of data.split("\n")) {
|
|
314
|
+
if (line.length > 0)
|
|
315
|
+
lines.push(line);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
// stdin not readable as a file — fall back to empty
|
|
320
|
+
}
|
|
321
|
+
let idx = 0;
|
|
322
|
+
return {
|
|
323
|
+
question: async (q) => {
|
|
324
|
+
process.stdout.write(q);
|
|
325
|
+
const answer = lines[idx++] ?? "";
|
|
326
|
+
// Echo a newline so subsequent prompts start on a new line (in TTY
|
|
327
|
+
// mode, readline echoes the user's Enter; piped mode does not).
|
|
328
|
+
process.stdout.write("\n");
|
|
329
|
+
return answer;
|
|
330
|
+
},
|
|
331
|
+
close: () => { },
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
// --- display ---
|
|
335
|
+
function formatDuration(ms) {
|
|
336
|
+
if (ms === null)
|
|
337
|
+
return "";
|
|
338
|
+
if (ms < 1000)
|
|
339
|
+
return "<1s";
|
|
340
|
+
const s = Math.floor(ms / 1000);
|
|
341
|
+
if (s < 60)
|
|
342
|
+
return `${s}s`;
|
|
343
|
+
const m = Math.floor(s / 60);
|
|
344
|
+
return `${m}m`;
|
|
345
|
+
}
|
|
346
|
+
function formatStartTime(iso) {
|
|
347
|
+
try {
|
|
348
|
+
const d = new Date(iso);
|
|
349
|
+
return d.toLocaleString("en-US", {
|
|
350
|
+
month: "short",
|
|
351
|
+
day: "2-digit",
|
|
352
|
+
hour: "2-digit",
|
|
353
|
+
minute: "2-digit",
|
|
354
|
+
hour12: false,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
return iso;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// Minimal ANSI escape sequences for the session list.
|
|
362
|
+
const RESET = "\x1b[0m";
|
|
363
|
+
const BOLD = "\x1b[1m";
|
|
364
|
+
const DIM = "\x1b[2m";
|
|
365
|
+
const GREEN = "\x1b[32m";
|
|
366
|
+
const CYAN = "\x1b[36m";
|
|
367
|
+
function renderSessionList(sessions) {
|
|
368
|
+
const sep = DIM + " " + "─".repeat(70) + RESET;
|
|
369
|
+
const lines = [BOLD + " Recent sessions" + RESET, sep];
|
|
370
|
+
const countWidth = String(sessions.length).length;
|
|
371
|
+
for (let i = 0; i < sessions.length; i++) {
|
|
372
|
+
const s = sessions[i];
|
|
373
|
+
const num = ` ${i + 1}.`.padEnd(countWidth + 2);
|
|
374
|
+
const idx = GREEN + num + RESET + " ";
|
|
375
|
+
const time = DIM + formatStartTime(s.startTime).padEnd(17) + RESET + " ";
|
|
376
|
+
const dur = DIM + formatDuration(s.durationMs).padEnd(5) + RESET + " ";
|
|
377
|
+
const msg = s.firstMessage || "(no text)";
|
|
378
|
+
lines.push(` ${idx}${time}${dur}${msg}`);
|
|
379
|
+
}
|
|
380
|
+
lines.push("");
|
|
381
|
+
return lines.join("\n");
|
|
382
|
+
}
|
|
383
|
+
// --- orchestration ---
|
|
384
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
385
|
+
export async function feedbackCommand(args, deps = {}, cliVersion) {
|
|
386
|
+
const writeOut = deps.writeOut ?? ((line) => void process.stdout.write(`${line}\n`));
|
|
387
|
+
const writeErr = deps.writeErr ?? ((line) => void process.stderr.write(`${line}\n`));
|
|
388
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
389
|
+
const env = deps.env ?? process.env;
|
|
390
|
+
// Load credentials (same pattern as goCommand).
|
|
391
|
+
const profile = await (deps.loadCredentials ?? (async () => {
|
|
392
|
+
const active = await readActiveProfile(env);
|
|
393
|
+
const creds = credentialsFromProfile(active);
|
|
394
|
+
return {
|
|
395
|
+
name: active.name,
|
|
396
|
+
baseUrl: active.baseUrl,
|
|
397
|
+
...(creds?.token ? { token: creds.token } : {}),
|
|
398
|
+
};
|
|
399
|
+
}))();
|
|
400
|
+
if (!profile.token) {
|
|
401
|
+
writeErr(`Not logged in to environment "${profile.name}" (${profile.baseUrl}). Run \`yagni login\` first.`);
|
|
402
|
+
return 1;
|
|
403
|
+
}
|
|
404
|
+
const agentDirPath = deps.agentDirPath ?? agentDir(profile.name);
|
|
405
|
+
const stateDir = deps.stateDir ?? credentialsDir();
|
|
406
|
+
// Parse args: is the first positional a session UUID?
|
|
407
|
+
const sessionIdArg = args.find((a) => !a.startsWith("-") && UUID_RE.test(a));
|
|
408
|
+
let sessionFile;
|
|
409
|
+
let sessionId;
|
|
410
|
+
// One readline interface for all prompts. For piped stdin, makeReadline
|
|
411
|
+
// reads all lines upfront to avoid the readline/promises hang on EOF.
|
|
412
|
+
const rl = deps.readline ?? makeReadline();
|
|
413
|
+
const closeRl = () => {
|
|
414
|
+
if (deps.readline)
|
|
415
|
+
deps.readline.close();
|
|
416
|
+
else
|
|
417
|
+
rl.close();
|
|
418
|
+
};
|
|
419
|
+
try {
|
|
420
|
+
if (sessionIdArg) {
|
|
421
|
+
// Case B: session ID provided directly.
|
|
422
|
+
const found = findSessionById(agentDirPath, sessionIdArg);
|
|
423
|
+
if (!found) {
|
|
424
|
+
writeErr(`Session ${sessionIdArg} not found.`);
|
|
425
|
+
return 1;
|
|
426
|
+
}
|
|
427
|
+
sessionFile = found.filePath;
|
|
428
|
+
sessionId = sessionIdArg;
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
// Case A: list sessions for the current cwd.
|
|
432
|
+
const sessions = listRecentSessions(agentDirPath, cwd);
|
|
433
|
+
if (sessions.length === 0) {
|
|
434
|
+
writeOut("No recent sessions found for this directory.");
|
|
435
|
+
return 1;
|
|
436
|
+
}
|
|
437
|
+
writeOut(renderSessionList(sessions));
|
|
438
|
+
const answer = (await rl.question(CYAN + "Select a session (1-" + sessions.length + "): " + RESET)).trim();
|
|
439
|
+
const idx = parseInt(answer, 10) - 1;
|
|
440
|
+
if (isNaN(idx) || idx < 0 || idx >= sessions.length) {
|
|
441
|
+
writeOut("Feedback cancelled.");
|
|
442
|
+
return 0;
|
|
443
|
+
}
|
|
444
|
+
sessionFile = sessions[idx].filePath;
|
|
445
|
+
sessionId = sessions[idx].id;
|
|
446
|
+
}
|
|
447
|
+
// Prompt for description.
|
|
448
|
+
const descAnswer = await rl.question(DIM + "Describe the issue (one or two lines):" + RESET + "\n> ");
|
|
449
|
+
const description = descAnswer.trim();
|
|
450
|
+
if (!description) {
|
|
451
|
+
writeOut("Feedback cancelled.");
|
|
452
|
+
return 0;
|
|
453
|
+
}
|
|
454
|
+
// Confirm.
|
|
455
|
+
const transcriptBytes = sessionFile
|
|
456
|
+
? Buffer.byteLength(readTranscript(sessionFile), "utf8")
|
|
457
|
+
: 0;
|
|
458
|
+
const confirmLines = [
|
|
459
|
+
DIM + "Sending:" + RESET,
|
|
460
|
+
` - Your feedback description`,
|
|
461
|
+
` - This session's transcript${transcriptBytes > 0 ? "" : " (could not be read)"}`,
|
|
462
|
+
` - Recent error trail for this session`,
|
|
463
|
+
"",
|
|
464
|
+
CYAN + "Send this report? [Y/n]" + RESET,
|
|
465
|
+
];
|
|
466
|
+
const confirmed = (await rl.question(confirmLines.join("\n") + "\n")).trim();
|
|
467
|
+
if (/^n/i.test(confirmed)) {
|
|
468
|
+
writeOut("Feedback cancelled.");
|
|
469
|
+
return 0;
|
|
470
|
+
}
|
|
471
|
+
// Build payload + submit.
|
|
472
|
+
const payload = buildFeedbackPayload({
|
|
473
|
+
sessionId,
|
|
474
|
+
sessionFile,
|
|
475
|
+
description,
|
|
476
|
+
stateDir,
|
|
477
|
+
env,
|
|
478
|
+
clientVersion: cliVersion ?? "unknown",
|
|
479
|
+
});
|
|
480
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
481
|
+
const result = await submitFeedback(payload, profile.baseUrl, profile.token, fetchImpl);
|
|
482
|
+
if (result.ok) {
|
|
483
|
+
writeOut(GREEN + "Feedback submitted. Thank you!" + RESET);
|
|
484
|
+
return 0;
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
writeErr(`Could not submit feedback. ${result.error ?? "Please try again."}`);
|
|
488
|
+
return 1;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
// readline closed or EOF — treat as cancel.
|
|
493
|
+
writeOut("Feedback cancelled.");
|
|
494
|
+
return 0;
|
|
495
|
+
}
|
|
496
|
+
finally {
|
|
497
|
+
closeRl();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
//# sourceMappingURL=feedback.js.map
|
package/dist/goHeadless.d.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* runs locally and what the fleet executes are the same binary and the same
|
|
16
16
|
* pipeline.
|
|
17
17
|
*/
|
|
18
|
+
import { type OtelLaunchConfig } from "./otel.js";
|
|
18
19
|
/** Mirrors HEADLESS_GO_EXIT in the extension; duplicated to keep the packages independent. */
|
|
19
20
|
export declare const GO_EXIT: {
|
|
20
21
|
readonly verified: 0;
|
|
@@ -64,6 +65,8 @@ export declare function buildHeadlessChildEnv(opts: {
|
|
|
64
65
|
workspaceId?: string;
|
|
65
66
|
cliVersion?: string;
|
|
66
67
|
sessionId?: string;
|
|
68
|
+
/** Resolved OTel export config; stage children load pi-otel when present. */
|
|
69
|
+
otel?: OtelLaunchConfig;
|
|
67
70
|
}): NodeJS.ProcessEnv;
|
|
68
71
|
/**
|
|
69
72
|
* Run `yagni go`. Returns the process exit code: 0 only on a verified
|
package/dist/goHeadless.js
CHANGED
|
@@ -20,6 +20,7 @@ import { mkdirSync } from "node:fs";
|
|
|
20
20
|
import { pathToFileURL } from "node:url";
|
|
21
21
|
import { agentDirEnvVar } from "./branding.js";
|
|
22
22
|
import { agentDir, credentialsDir } from "./credentials.js";
|
|
23
|
+
import { otelChildEnv, resolveOtelLaunchWithWorkspace } from "./otel.js";
|
|
23
24
|
import { credentialsFromProfile, profilePath, readActiveProfile } from "./profiles.js";
|
|
24
25
|
import { resolveHeadlessGoPath, resolvePiCliPath } from "./paths.js";
|
|
25
26
|
/** Mirrors HEADLESS_GO_EXIT in the extension; duplicated to keep the packages independent. */
|
|
@@ -54,6 +55,9 @@ export function buildHeadlessChildEnv(opts) {
|
|
|
54
55
|
PI_CODING_AGENT_DIR: piAgentDir,
|
|
55
56
|
PI_SKIP_VERSION_CHECK: "1",
|
|
56
57
|
PI_TELEMETRY: "0",
|
|
58
|
+
// OTel export: pin metadata-only capture and forward pi-otel's path so
|
|
59
|
+
// every stage child spawns with it (see otel.ts for the policy).
|
|
60
|
+
...(opts.otel ? otelChildEnv(opts.otel, opts.baseEnv) : {}),
|
|
57
61
|
};
|
|
58
62
|
}
|
|
59
63
|
/**
|
|
@@ -88,6 +92,14 @@ export async function goCommand(args, deps = {}, cliVersion) {
|
|
|
88
92
|
writeErr(`Not logged in to environment "${profile.name}" (${profile.baseUrl}). Run \`yagni login\` first.`);
|
|
89
93
|
return GO_EXIT.usage;
|
|
90
94
|
}
|
|
95
|
+
// Same OTel gate as an interactive launch — a headless /go run's stage
|
|
96
|
+
// children are LLM spend too, and the pilot's cost A/B needs to see them.
|
|
97
|
+
const otel = await resolveOtelLaunchWithWorkspace({
|
|
98
|
+
env: baseEnv,
|
|
99
|
+
cwd: deps.cwd ?? process.cwd(),
|
|
100
|
+
creds: { baseUrl, token },
|
|
101
|
+
profileName: profile.name,
|
|
102
|
+
});
|
|
91
103
|
const childEnv = buildHeadlessChildEnv({
|
|
92
104
|
baseEnv,
|
|
93
105
|
token,
|
|
@@ -96,6 +108,7 @@ export async function goCommand(args, deps = {}, cliVersion) {
|
|
|
96
108
|
...(profile.expiresAt ? { expiresAt: profile.expiresAt } : {}),
|
|
97
109
|
...(profile.workspaceId ? { workspaceId: profile.workspaceId } : {}),
|
|
98
110
|
...(cliVersion ? { cliVersion } : {}),
|
|
111
|
+
...(otel ? { otel } : {}),
|
|
99
112
|
});
|
|
100
113
|
// The hermetic agent dir must exist before a stage child tries to read it.
|
|
101
114
|
try {
|
package/dist/launch.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* defaults the provider to `yagni`.
|
|
7
7
|
*/
|
|
8
8
|
import type { Credentials } from "./credentials.js";
|
|
9
|
+
import { type OtelLaunchConfig } from "./otel.js";
|
|
9
10
|
export interface LaunchPlan {
|
|
10
11
|
env: NodeJS.ProcessEnv;
|
|
11
12
|
argv: string[];
|
|
@@ -92,6 +93,13 @@ export interface BuildLaunchOptions {
|
|
|
92
93
|
* override the token, base URL, or hermetic agent dir.
|
|
93
94
|
*/
|
|
94
95
|
extraEnv?: Record<string, string>;
|
|
96
|
+
/**
|
|
97
|
+
* Resolved OTel export config (see `otel.ts`). When present, the session
|
|
98
|
+
* loads pi-otel alongside our extension and the env pins metadata-only
|
|
99
|
+
* capture; absent means no OTLP endpoint is configured and the launch is
|
|
100
|
+
* byte-for-byte what it was before OTel support existed.
|
|
101
|
+
*/
|
|
102
|
+
otel?: OtelLaunchConfig;
|
|
95
103
|
}
|
|
96
104
|
export declare function buildLaunch(creds: Credentials | null, passthroughArgs: string[], opts: BuildLaunchOptions): LaunchPlan;
|
|
97
105
|
//# sourceMappingURL=launch.d.ts.map
|
package/dist/launch.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { randomUUID } from "node:crypto";
|
|
9
9
|
import { agentDirEnvVar } from "./branding.js";
|
|
10
|
+
import { otelChildEnv } from "./otel.js";
|
|
10
11
|
import { PAD_X_ENV, resolvePadX } from "./padding.js";
|
|
11
12
|
import { ENGINEERING_PRACTICE_SECTION, promptEnrichmentDisabled } from "./promptEnrichment.js";
|
|
12
13
|
/**
|
|
@@ -105,6 +106,10 @@ export function buildLaunch(creds, passthroughArgs, opts) {
|
|
|
105
106
|
// the editor (which the launcher pads by seeding editorPaddingX). The
|
|
106
107
|
// footer can't read pi's settings, so the value crosses over env.
|
|
107
108
|
[PAD_X_ENV]: String(resolvePadX()),
|
|
109
|
+
// OTel export (gated in otel.ts): pin metadata-only capture and forward
|
|
110
|
+
// pi-otel's path so /go stage children and subagents load it too. Placed
|
|
111
|
+
// after baseEnv on purpose — the capture pin must beat a user env override.
|
|
112
|
+
...(opts.otel ? otelChildEnv(opts.otel, opts.baseEnv ?? {}) : {}),
|
|
108
113
|
};
|
|
109
114
|
// Always load our extension. Default the provider to `yagni` unless the user
|
|
110
115
|
// explicitly chose one (so power users can still point pi elsewhere).
|
|
@@ -125,6 +130,7 @@ export function buildLaunch(creds, passthroughArgs, opts) {
|
|
|
125
130
|
const argv = [
|
|
126
131
|
"-e",
|
|
127
132
|
opts.extensionPath,
|
|
133
|
+
...(opts.otel ? ["-e", opts.otel.extensionPath] : []),
|
|
128
134
|
...(userChoseProvider ? [] : ["--provider", "yagni"]),
|
|
129
135
|
...(userChoseModel ? [] : ["--model", "advanced"]),
|
|
130
136
|
...(enrichmentOff ? [] : ["--append-system-prompt", ENGINEERING_PRACTICE_SECTION]),
|