@yagni-app/code 0.2.1 → 0.3.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/dist/cli.d.ts +30 -0
- package/dist/cli.js +147 -3
- package/dist/connectClaudeCode.d.ts +77 -0
- package/dist/connectClaudeCode.js +228 -0
- package/dist/connectCodex.d.ts +75 -0
- package/dist/connectCodex.js +201 -0
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +1 -1
- package/dist/extension/advisor.d.ts +4 -4
- package/dist/extension/advisor.js +6 -7
- package/dist/extension/approvedPrefixes.d.ts +103 -0
- package/dist/extension/approvedPrefixes.js +282 -0
- package/dist/extension/askAdvisorTool.d.ts +20 -5
- package/dist/extension/askAdvisorTool.js +126 -20
- package/dist/extension/askYagniTool.d.ts +23 -0
- package/dist/extension/askYagniTool.js +91 -2
- package/dist/extension/branding.d.ts +34 -3
- package/dist/extension/branding.js +114 -13
- package/dist/extension/chipEditor.d.ts +30 -9
- package/dist/extension/chipEditor.js +173 -59
- package/dist/extension/claudeRules.d.ts +0 -2
- package/dist/extension/claudeRules.js +0 -8
- package/dist/extension/cmux/dispatcher.d.ts +25 -0
- package/dist/extension/cmux/dispatcher.js +266 -0
- package/dist/extension/cmux/hooks.d.ts +12 -0
- package/dist/extension/cmux/hooks.js +192 -0
- package/dist/extension/cmux/index.d.ts +3 -0
- package/dist/extension/cmux/index.js +155 -0
- package/dist/extension/cmux/naming.d.ts +5 -0
- package/dist/extension/cmux/naming.js +23 -0
- package/dist/extension/cmux/state.d.ts +33 -0
- package/dist/extension/cmux/state.js +142 -0
- package/dist/extension/config.d.ts +44 -1
- package/dist/extension/config.js +37 -4
- package/dist/extension/costHud.d.ts +16 -22
- package/dist/extension/costHud.js +8 -47
- package/dist/extension/crashReport.js +1 -3
- package/dist/extension/execPolicy.d.ts +135 -0
- package/dist/extension/execPolicy.js +936 -0
- package/dist/extension/flywheel.d.ts +44 -0
- package/dist/extension/flywheel.js +53 -0
- package/dist/extension/footer.d.ts +118 -0
- package/dist/extension/footer.js +308 -0
- package/dist/extension/guardian.d.ts +139 -0
- package/dist/extension/guardian.js +237 -0
- package/dist/extension/index.d.ts +35 -7
- package/dist/extension/index.js +338 -33
- package/dist/extension/mineBeat.d.ts +95 -0
- package/dist/extension/mineBeat.js +193 -0
- package/dist/extension/permission.d.ts +124 -10
- package/dist/extension/permission.js +603 -52
- package/dist/extension/pipeline/childRegistry.d.ts +41 -0
- package/dist/extension/pipeline/childRegistry.js +118 -0
- package/dist/extension/pipeline/finish.js +5 -1
- package/dist/extension/pipeline/goCommand.d.ts +1 -1
- package/dist/extension/pipeline/goCommand.js +40 -9
- package/dist/extension/pipeline/goStatusCommands.d.ts +10 -0
- package/dist/extension/pipeline/goStatusCommands.js +61 -1
- package/dist/extension/pipeline/personas.js +26 -1
- package/dist/extension/pipeline/resilience.d.ts +2 -1
- package/dist/extension/pipeline/resilience.js +21 -2
- package/dist/extension/pipeline/runRegistry.d.ts +23 -1
- package/dist/extension/pipeline/runRegistry.js +57 -1
- package/dist/extension/pipeline/runner.js +4 -0
- package/dist/extension/pipeline/verify.d.ts +4 -0
- package/dist/extension/pipeline/verify.js +48 -26
- package/dist/extension/recordDecisionTool.d.ts +8 -0
- package/dist/extension/recordDecisionTool.js +24 -0
- package/dist/extension/redact.d.ts +20 -0
- package/dist/extension/redact.js +64 -0
- package/dist/extension/rerouteNotice.d.ts +3 -4
- package/dist/extension/rerouteNotice.js +20 -11
- package/dist/extension/subagentRender.d.ts +129 -0
- package/dist/extension/subagentRender.js +441 -0
- package/dist/extension/subagents.d.ts +11 -8
- package/dist/extension/subagents.js +162 -37
- package/dist/extension/ticketTools.d.ts +37 -0
- package/dist/extension/ticketTools.js +117 -0
- package/dist/extension/todos.d.ts +28 -1
- package/dist/extension/todos.js +76 -1
- package/dist/extension/tokenProvider.js +46 -5
- package/dist/extension/ultra.d.ts +27 -0
- package/dist/extension/ultra.js +76 -0
- package/dist/launch.d.ts +7 -0
- package/dist/launch.js +24 -12
- package/dist/login.d.ts +4 -2
- package/dist/login.js +19 -4
- package/dist/padding.d.ts +22 -0
- package/dist/padding.js +25 -0
- package/dist/promptEnrichment.d.ts +40 -0
- package/dist/promptEnrichment.js +85 -0
- package/dist/signalForward.d.ts +60 -0
- package/dist/signalForward.js +130 -0
- package/dist/token.d.ts +25 -0
- package/dist/token.js +45 -0
- package/package.json +6 -5
- package/dist/extension/boostCommand.d.ts +0 -144
- package/dist/extension/boostCommand.js +0 -263
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
export interface PendingCompletion {
|
|
3
|
+
lastAssistantMessage?: string;
|
|
4
|
+
notificationType: string;
|
|
5
|
+
turnId: string;
|
|
6
|
+
}
|
|
7
|
+
export interface SessionState {
|
|
8
|
+
nextTurn: number;
|
|
9
|
+
activeTurnId?: string;
|
|
10
|
+
pendingCompletion?: PendingCompletion;
|
|
11
|
+
stopped: boolean;
|
|
12
|
+
lastPrompt?: string;
|
|
13
|
+
hasNamed?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface PiExtensionContextSnapshot {
|
|
16
|
+
readonly sessionId: string | null;
|
|
17
|
+
readonly cwd: string;
|
|
18
|
+
readonly notifyWarning?: () => void;
|
|
19
|
+
}
|
|
20
|
+
export declare function firstString(...values: unknown[]): string | null;
|
|
21
|
+
export declare function objectValue(value: unknown, keys: string[]): unknown;
|
|
22
|
+
export declare function textFromContent(content: unknown): string | null;
|
|
23
|
+
export declare function lastAssistantMessage(event: unknown): string | undefined;
|
|
24
|
+
export declare function sessionIdFrom(ctx: ExtensionContext): string | null;
|
|
25
|
+
export declare function cwdFrom(ctx: ExtensionContext): string;
|
|
26
|
+
export declare function snapshotContext(ctx: ExtensionContext): PiExtensionContextSnapshot;
|
|
27
|
+
export declare function stateFor(sessionStates: Map<string, SessionState>, sessionId: string): SessionState;
|
|
28
|
+
export declare function beginTurn(sessionStates: Map<string, SessionState>, sessionId: string, event: unknown): string;
|
|
29
|
+
export declare function currentTurnId(sessionStates: Map<string, SessionState>, sessionId: string, event: unknown): string;
|
|
30
|
+
export declare function finishTurn(sessionStates: Map<string, SessionState>, sessionId: string, event: unknown): string;
|
|
31
|
+
export declare function settleTurn(sessionStates: Map<string, SessionState>, sessionId: string): PendingCompletion | undefined;
|
|
32
|
+
export declare function warn(ctx: PiExtensionContextSnapshot | null, message: string, details?: Record<string, unknown>, notifyUser?: boolean): void;
|
|
33
|
+
//# sourceMappingURL=state.d.ts.map
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
export function firstString(...values) {
|
|
2
|
+
for (const value of values) {
|
|
3
|
+
if (typeof value === "string" && value.trim().length > 0)
|
|
4
|
+
return value.trim();
|
|
5
|
+
}
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
export function objectValue(value, keys) {
|
|
9
|
+
if (!value || typeof value !== "object")
|
|
10
|
+
return undefined;
|
|
11
|
+
const typed = value;
|
|
12
|
+
for (const key of keys) {
|
|
13
|
+
if (typed[key] !== undefined && typed[key] !== null)
|
|
14
|
+
return typed[key];
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
export function textFromContent(content) {
|
|
19
|
+
if (typeof content === "string")
|
|
20
|
+
return content;
|
|
21
|
+
if (!Array.isArray(content))
|
|
22
|
+
return null;
|
|
23
|
+
const parts = [];
|
|
24
|
+
for (const block of content) {
|
|
25
|
+
if (!block || typeof block !== "object")
|
|
26
|
+
continue;
|
|
27
|
+
const typed = block;
|
|
28
|
+
if (typed.type === "text" && typeof typed.text === "string")
|
|
29
|
+
parts.push(typed.text);
|
|
30
|
+
}
|
|
31
|
+
return parts.join("\n") || null;
|
|
32
|
+
}
|
|
33
|
+
export function lastAssistantMessage(event) {
|
|
34
|
+
const messagesValue = objectValue(event, ["messages"]);
|
|
35
|
+
const messages = Array.isArray(messagesValue) ? messagesValue : [];
|
|
36
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
37
|
+
const message = messages[index];
|
|
38
|
+
if (!message || typeof message !== "object")
|
|
39
|
+
continue;
|
|
40
|
+
const typed = message;
|
|
41
|
+
if (typed.role !== "assistant")
|
|
42
|
+
continue;
|
|
43
|
+
const text = firstString(textFromContent(typed.content));
|
|
44
|
+
if (text)
|
|
45
|
+
return text;
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
export function sessionIdFrom(ctx) {
|
|
50
|
+
try {
|
|
51
|
+
return firstString(ctx.sessionManager?.getSessionId?.());
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function cwdFrom(ctx) {
|
|
58
|
+
try {
|
|
59
|
+
return firstString(ctx.cwd, process.cwd()) || process.cwd();
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return process.cwd();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function snapshotContext(ctx) {
|
|
66
|
+
if (!ctx || typeof ctx !== "object") {
|
|
67
|
+
return { sessionId: null, cwd: process.cwd() };
|
|
68
|
+
}
|
|
69
|
+
let notifyWarning;
|
|
70
|
+
try {
|
|
71
|
+
const ui = ctx.ui;
|
|
72
|
+
if (typeof ui?.notify === "function") {
|
|
73
|
+
notifyWarning = () => ui.notify?.("cmux integration warning — check the terminal for details", "warning");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch { }
|
|
77
|
+
return { sessionId: sessionIdFrom(ctx), cwd: cwdFrom(ctx), notifyWarning };
|
|
78
|
+
}
|
|
79
|
+
export function stateFor(sessionStates, sessionId) {
|
|
80
|
+
let state = sessionStates.get(sessionId);
|
|
81
|
+
if (!state) {
|
|
82
|
+
state = { nextTurn: 0, stopped: false };
|
|
83
|
+
sessionStates.set(sessionId, state);
|
|
84
|
+
}
|
|
85
|
+
return state;
|
|
86
|
+
}
|
|
87
|
+
function eventTurnId(event) {
|
|
88
|
+
return firstString(objectValue(event, ["turn_id", "turnId", "turnID"]));
|
|
89
|
+
}
|
|
90
|
+
export function beginTurn(sessionStates, sessionId, event) {
|
|
91
|
+
const state = stateFor(sessionStates, sessionId);
|
|
92
|
+
const turnId = eventTurnId(event) || `${sessionId}:turn-${state.nextTurn + 1}`;
|
|
93
|
+
if (!eventTurnId(event))
|
|
94
|
+
state.nextTurn += 1;
|
|
95
|
+
state.activeTurnId = turnId;
|
|
96
|
+
state.pendingCompletion = undefined;
|
|
97
|
+
state.stopped = false;
|
|
98
|
+
return turnId;
|
|
99
|
+
}
|
|
100
|
+
export function currentTurnId(sessionStates, sessionId, event) {
|
|
101
|
+
const state = stateFor(sessionStates, sessionId);
|
|
102
|
+
const turnId = eventTurnId(event) || state.activeTurnId || `${sessionId}:turn-${state.nextTurn + 1}`;
|
|
103
|
+
if (!eventTurnId(event) && !state.activeTurnId)
|
|
104
|
+
state.nextTurn += 1;
|
|
105
|
+
return turnId;
|
|
106
|
+
}
|
|
107
|
+
export function finishTurn(sessionStates, sessionId, event) {
|
|
108
|
+
const state = stateFor(sessionStates, sessionId);
|
|
109
|
+
const turnId = eventTurnId(event) || state.activeTurnId || `${sessionId}:turn-${state.nextTurn + 1}`;
|
|
110
|
+
if (!eventTurnId(event) && !state.activeTurnId)
|
|
111
|
+
state.nextTurn += 1;
|
|
112
|
+
state.activeTurnId = undefined;
|
|
113
|
+
state.pendingCompletion = undefined;
|
|
114
|
+
state.stopped = true;
|
|
115
|
+
return turnId;
|
|
116
|
+
}
|
|
117
|
+
export function settleTurn(sessionStates, sessionId) {
|
|
118
|
+
const state = sessionStates.get(sessionId);
|
|
119
|
+
const completion = state?.pendingCompletion;
|
|
120
|
+
if (!state || !completion || state.stopped)
|
|
121
|
+
return undefined;
|
|
122
|
+
state.activeTurnId = undefined;
|
|
123
|
+
state.pendingCompletion = undefined;
|
|
124
|
+
state.stopped = true;
|
|
125
|
+
return completion;
|
|
126
|
+
}
|
|
127
|
+
export function warn(ctx, message, details = {}, notifyUser = false) {
|
|
128
|
+
const payload = { source: "yagni-cmux-bridge", level: "warning", message, ...details };
|
|
129
|
+
try {
|
|
130
|
+
console.warn(JSON.stringify(payload));
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
console.warn(`[yagni-cmux-bridge] ${message}`);
|
|
134
|
+
}
|
|
135
|
+
if (notifyUser) {
|
|
136
|
+
try {
|
|
137
|
+
ctx?.notifyWarning?.();
|
|
138
|
+
}
|
|
139
|
+
catch { }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=state.js.map
|
|
@@ -72,13 +72,32 @@ export interface FetchCatalogOptions {
|
|
|
72
72
|
getToken: () => string | undefined;
|
|
73
73
|
fetchImpl?: typeof fetch;
|
|
74
74
|
}
|
|
75
|
+
/** Guardian storage tier for this workspace (YAG-510). */
|
|
76
|
+
export type GuardianStorageTier = "off" | "hash" | "raw";
|
|
77
|
+
/** The startup catalog response from GET /api/yagni-code/models. */
|
|
78
|
+
export interface CatalogResult {
|
|
79
|
+
models: ModelEntry[];
|
|
80
|
+
/**
|
|
81
|
+
* Per-workspace Guardian kill switch (yagni_code.guardian). Fail-safe:
|
|
82
|
+
* only an explicit `false` from the backend disables the Guardian — a
|
|
83
|
+
* missing field (older backend) reads as enabled.
|
|
84
|
+
*/
|
|
85
|
+
guardianEnabled: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Guardian event storage tier (YAG-510). Polarity is deliberately the
|
|
88
|
+
* OPPOSITE of guardianEnabled: only an explicit "hash" or "raw" turns
|
|
89
|
+
* storage on — a missing field (older backend) or unknown value reads as
|
|
90
|
+
* "off" (fail toward not sending).
|
|
91
|
+
*/
|
|
92
|
+
guardianStorage: GuardianStorageTier;
|
|
93
|
+
}
|
|
75
94
|
/**
|
|
76
95
|
* Fetch the YAGNI model catalog at startup.
|
|
77
96
|
*
|
|
78
97
|
* @throws an actionable Error (mentioning `yagni login`) on any non-2xx
|
|
79
98
|
* response so the launcher can surface a clear re-authentication prompt.
|
|
80
99
|
*/
|
|
81
|
-
export declare function fetchCatalog(opts: FetchCatalogOptions): Promise<
|
|
100
|
+
export declare function fetchCatalog(opts: FetchCatalogOptions): Promise<CatalogResult>;
|
|
82
101
|
/** The startup company brief returned by GET /api/yagni-code/context. */
|
|
83
102
|
export interface ContextBrief {
|
|
84
103
|
brief: string;
|
|
@@ -94,6 +113,12 @@ export interface ContextBrief {
|
|
|
94
113
|
decisions: number;
|
|
95
114
|
corrections: number;
|
|
96
115
|
};
|
|
116
|
+
/**
|
|
117
|
+
* Active decisions scoped to the repo named on the request (additive, Run
|
|
118
|
+
* 7; absent without `?repo=`, on older backends, and on a count failure).
|
|
119
|
+
* The opt-in mining beat fires ONLY on an explicit 0.
|
|
120
|
+
*/
|
|
121
|
+
repoDecisionCount?: number;
|
|
97
122
|
}
|
|
98
123
|
/**
|
|
99
124
|
* Attribution headers for the model proxy (YAG-471). On the SERVER side these
|
|
@@ -156,11 +181,29 @@ export declare function isDriverCaller(env?: NodeJS.ProcessEnv): boolean;
|
|
|
156
181
|
* sanitizing client-side is what keeps the attribution instead of losing it.
|
|
157
182
|
*/
|
|
158
183
|
export declare function sanitizeCallerSegment(name: string, maxLength?: number): string;
|
|
184
|
+
/**
|
|
185
|
+
* Attribution headers for the boot-time /context fetch. Extends
|
|
186
|
+
* {@link attributionHeaders} (session id + caller) with the surface the
|
|
187
|
+
* session is painting for (the desktop driver sets YAGNI_SURFACE=desktop;
|
|
188
|
+
* the terminal CLI sets nothing) and the shipping client version. The server
|
|
189
|
+
* uses these to emit its "YAGNI Code Session Started" analytics event —
|
|
190
|
+
* labels only on that side: a missing or malformed value can never fail the
|
|
191
|
+
* fetch, it just goes uncounted or unversioned.
|
|
192
|
+
*/
|
|
193
|
+
export declare function sessionTelemetryHeaders(env?: NodeJS.ProcessEnv): Record<string, string>;
|
|
159
194
|
/** Options for {@link fetchContextBrief}. */
|
|
160
195
|
export interface FetchContextBriefOptions {
|
|
161
196
|
baseUrl: string;
|
|
162
197
|
getToken: () => string | undefined;
|
|
163
198
|
fetchImpl?: typeof fetch;
|
|
199
|
+
/** Env seam for the telemetry headers; defaults to process.env. */
|
|
200
|
+
env?: NodeJS.ProcessEnv;
|
|
201
|
+
/**
|
|
202
|
+
* The session repo (`owner/name`), when resolvable. Rides as `?repo=` so
|
|
203
|
+
* the response can carry `repoDecisionCount` for the mining beat — no
|
|
204
|
+
* second round-trip at boot.
|
|
205
|
+
*/
|
|
206
|
+
repo?: string;
|
|
164
207
|
}
|
|
165
208
|
/**
|
|
166
209
|
* Fetch the workspace company brief at startup.
|
package/dist/extension/config.js
CHANGED
|
@@ -85,7 +85,13 @@ export async function fetchCatalog(opts) {
|
|
|
85
85
|
throw new Error(`Failed to fetch YAGNI model catalog (HTTP ${res.status}). Run \`yagni login\` to re-authenticate.`);
|
|
86
86
|
}
|
|
87
87
|
const data = (await res.json());
|
|
88
|
-
return
|
|
88
|
+
return {
|
|
89
|
+
models: data.models,
|
|
90
|
+
guardianEnabled: data.guardianEnabled !== false,
|
|
91
|
+
guardianStorage: data.guardianStorage === "raw" || data.guardianStorage === "hash"
|
|
92
|
+
? data.guardianStorage
|
|
93
|
+
: "off",
|
|
94
|
+
};
|
|
89
95
|
}
|
|
90
96
|
/** Shape-check for a caller label: mirrors the model proxy's own validation regex. */
|
|
91
97
|
const CALLER_LABEL_RE = /^[a-z0-9][a-z0-9:_.-]{0,63}$/i;
|
|
@@ -129,8 +135,6 @@ export function attributionHeaders(env = process.env) {
|
|
|
129
135
|
headers["x-yagni-run-id"] = runId;
|
|
130
136
|
const caller = env.YAGNI_CALLER ?? "";
|
|
131
137
|
headers["x-yagni-caller"] = CALLER_LABEL_RE.test(caller) ? caller : "driver";
|
|
132
|
-
if (env.YAGNI_BOOST === "1")
|
|
133
|
-
headers["x-yagni-boost"] = "1";
|
|
134
138
|
return headers;
|
|
135
139
|
}
|
|
136
140
|
/**
|
|
@@ -173,6 +177,28 @@ export function sanitizeCallerSegment(name, maxLength = 64) {
|
|
|
173
177
|
const trimmed = cleaned.slice(0, Math.max(0, maxLength));
|
|
174
178
|
return trimmed || "agent";
|
|
175
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Shape-check for a client version label (semver-ish; matches the server's
|
|
182
|
+
* own allowlist). A value that fails is dropped, never sent raw.
|
|
183
|
+
*/
|
|
184
|
+
const CLIENT_VERSION_RE = /^[0-9a-z][0-9a-z.+-]{0,31}$/i;
|
|
185
|
+
/**
|
|
186
|
+
* Attribution headers for the boot-time /context fetch. Extends
|
|
187
|
+
* {@link attributionHeaders} (session id + caller) with the surface the
|
|
188
|
+
* session is painting for (the desktop driver sets YAGNI_SURFACE=desktop;
|
|
189
|
+
* the terminal CLI sets nothing) and the shipping client version. The server
|
|
190
|
+
* uses these to emit its "YAGNI Code Session Started" analytics event —
|
|
191
|
+
* labels only on that side: a missing or malformed value can never fail the
|
|
192
|
+
* fetch, it just goes uncounted or unversioned.
|
|
193
|
+
*/
|
|
194
|
+
export function sessionTelemetryHeaders(env = process.env) {
|
|
195
|
+
const headers = attributionHeaders(env);
|
|
196
|
+
headers["x-yagni-surface"] = env.YAGNI_SURFACE === "desktop" ? "desktop" : "cli";
|
|
197
|
+
const version = env.YAGNI_CODE_VERSION ?? "";
|
|
198
|
+
if (CLIENT_VERSION_RE.test(version))
|
|
199
|
+
headers["x-yagni-client-version"] = version;
|
|
200
|
+
return headers;
|
|
201
|
+
}
|
|
176
202
|
/**
|
|
177
203
|
* Fetch the workspace company brief at startup.
|
|
178
204
|
*
|
|
@@ -182,7 +208,14 @@ export function sanitizeCallerSegment(name, maxLength = 64) {
|
|
|
182
208
|
*/
|
|
183
209
|
export async function fetchContextBrief(opts) {
|
|
184
210
|
try {
|
|
185
|
-
const
|
|
211
|
+
const query = opts.repo ? `?repo=${encodeURIComponent(opts.repo)}` : "";
|
|
212
|
+
const res = await resilientFetch(`${opts.baseUrl}/api/yagni-code/context${query}`, {
|
|
213
|
+
method: "GET",
|
|
214
|
+
headers: {
|
|
215
|
+
authorization: `Bearer ${opts.getToken() ?? ""}`,
|
|
216
|
+
...sessionTelemetryHeaders(opts.env),
|
|
217
|
+
},
|
|
218
|
+
}, { fetchImpl: opts.fetchImpl });
|
|
186
219
|
if (!res.ok)
|
|
187
220
|
return null;
|
|
188
221
|
return (await res.json());
|
|
@@ -52,7 +52,20 @@ export interface Headroom {
|
|
|
52
52
|
remaining: number;
|
|
53
53
|
unit: string;
|
|
54
54
|
}
|
|
55
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Render the /cost line. Pure, no em-dashes. `headroom` null means it was
|
|
57
|
+
* unavailable. `advisorLine` is the ask_advisor subtotal (empty when the session
|
|
58
|
+
* never escalated). `source`, when given, is parenthesized right after "Session
|
|
59
|
+
* usage", used to explicitly label this as the LOCAL, driver-only fallback when
|
|
60
|
+
* the server-authoritative spend fetch is unavailable (see
|
|
61
|
+
* {@link formatServerCostLines}, the preferred path).
|
|
62
|
+
*
|
|
63
|
+
* HONESTY NOTE: this counter can only see the DRIVER session, because it is fed
|
|
64
|
+
* by pi's `turn_end`. Work that runs in a child process — every /go stage, and
|
|
65
|
+
* every advisor consult — never emits a parent `turn_end`, so it is invisible
|
|
66
|
+
* here. The advisor subtotal is threaded in explicitly for exactly that reason.
|
|
67
|
+
*/
|
|
68
|
+
export declare function formatCostLine(snap: CostSnapshot, headroom?: Headroom | null, advisorLine?: string, source?: string): string;
|
|
56
69
|
/**
|
|
57
70
|
* One row of `GET /api/yagni-code/spend`'s per-caller x rate-tier breakdown.
|
|
58
71
|
* Mirrored locally rather than importing `@yagni/shared`, since this extension
|
|
@@ -102,25 +115,15 @@ export declare const usd: (millicents: number) => string;
|
|
|
102
115
|
* (joined with "\n").
|
|
103
116
|
*
|
|
104
117
|
* Line order: total spend, one line per rate tier (aggregated across callers,
|
|
105
|
-
* sorted by spend descending),
|
|
106
|
-
* boosted rows, a live-toggle boost line (see below), the savings line (only
|
|
118
|
+
* sorted by spend descending), the savings line (only
|
|
107
119
|
* when a counterfactual total resolved), an incomplete-counterfactual note,
|
|
108
120
|
* an unbilled note, a dropped-run-ids note (carry-over from the /cost
|
|
109
121
|
* re-review — see sessionRuns.ts's `droppedSessionRuns`), then headroom.
|
|
110
122
|
*
|
|
111
123
|
* `droppedRunCount` defaults to 0 (no note) so every existing direct caller
|
|
112
124
|
* of this pure function keeps behaving identically without passing it.
|
|
113
|
-
*
|
|
114
|
-
* `boosted` (default false) is the LIVE client-side toggle from
|
|
115
|
-
* `boostCommand.ts`'s `isBoosted()`, threaded in exactly like `advisorLine`
|
|
116
|
-
* is for {@link formatCostLine}. It is independent of the per-tier "Boosted
|
|
117
|
-
* (tier) spend" subtotal above: that subtotal only reflects server rows
|
|
118
|
-
* carrying `x-yagni-boost`, which only `/go` children (and other spawned
|
|
119
|
-
* processes) ever send — the driver's own turns never do (see
|
|
120
|
-
* boostCommand.ts's KNOWN-asymmetry docblock). Without this flag, a session
|
|
121
|
-
* that only chats while boosted would show no boost line at all.
|
|
122
125
|
*/
|
|
123
|
-
export declare function formatServerCostLines(spend: SpendResponse, headroom?: Headroom | null, droppedRunCount?: number
|
|
126
|
+
export declare function formatServerCostLines(spend: SpendResponse, headroom?: Headroom | null, droppedRunCount?: number): string;
|
|
124
127
|
export interface RegisterCostDeps {
|
|
125
128
|
/** Fetch remaining credit headroom; return null when unavailable. Fail-soft. */
|
|
126
129
|
fetchHeadroom?: (signal?: AbortSignal) => Promise<Headroom | null>;
|
|
@@ -132,15 +135,6 @@ export interface RegisterCostDeps {
|
|
|
132
135
|
* ordinary caller row.
|
|
133
136
|
*/
|
|
134
137
|
advisorSubtotal?: () => string;
|
|
135
|
-
/**
|
|
136
|
-
* Whether the session is currently boosted to Peak (boostCommand.ts's
|
|
137
|
-
* `isBoosted()`, threaded in the same way as `advisorSubtotal`). Unlike
|
|
138
|
-
* `advisorSubtotal`, this is read on BOTH the server-authoritative and the
|
|
139
|
-
* local-fallback branch: the driver's own turns never carry a server-side
|
|
140
|
-
* boost marker (see boostCommand.ts's KNOWN-asymmetry docblock), so this is
|
|
141
|
-
* the only signal that would otherwise be missing from the server branch.
|
|
142
|
-
*/
|
|
143
|
-
isBoosted?: () => boolean;
|
|
144
138
|
/**
|
|
145
139
|
* Fetch the server-authoritative session spend (YAG-383). Absent, throwing,
|
|
146
140
|
* or resolving null all fall back to the local `turn_end` accumulator, with
|
|
@@ -75,21 +75,18 @@ const fmt = (n) => n.toLocaleString("en-US");
|
|
|
75
75
|
* every advisor consult — never emits a parent `turn_end`, so it is invisible
|
|
76
76
|
* here. The advisor subtotal is threaded in explicitly for exactly that reason.
|
|
77
77
|
*/
|
|
78
|
-
|
|
79
|
-
const BOOST_LINE = "Boost is on. Driver turns bill at the peak tier.";
|
|
80
|
-
export function formatCostLine(snap, headroom, advisorLine, source, boosted = false) {
|
|
78
|
+
export function formatCostLine(snap, headroom, advisorLine, source) {
|
|
81
79
|
const turns = `${snap.turns} turn${snap.turns === 1 ? "" : "s"}`;
|
|
82
80
|
const cached = snap.cacheRead > 0 ? ` (${fmt(snap.cacheRead)} cached)` : "";
|
|
83
81
|
const label = source ? `Session usage (${source})` : "Session usage";
|
|
84
82
|
const base = `${label}: ${turns}, ${fmt(snap.input)} in / ${fmt(snap.output)} out tokens${cached}, ` +
|
|
85
83
|
`$${snap.cost.toFixed(2)} this session.`;
|
|
86
84
|
const advisor = advisorLine?.trim() ? ` ${advisorLine.trim()}` : "";
|
|
87
|
-
const boost = boosted ? ` ${BOOST_LINE}` : "";
|
|
88
85
|
if (headroom)
|
|
89
|
-
return `${base}${advisor}
|
|
86
|
+
return `${base}${advisor} Credit headroom: ${headroom.remaining} ${headroom.unit}.`;
|
|
90
87
|
if (headroom === null)
|
|
91
|
-
return `${base}${advisor}
|
|
92
|
-
return `${base}${advisor}
|
|
88
|
+
return `${base}${advisor} Credit headroom unavailable right now.`;
|
|
89
|
+
return `${base}${advisor}`;
|
|
93
90
|
}
|
|
94
91
|
/**
|
|
95
92
|
* Millicents (1/1000 of a cent) -> dollars, 2 decimals. Guards against a
|
|
@@ -115,25 +112,15 @@ export const usd = (millicents) => {
|
|
|
115
112
|
* (joined with "\n").
|
|
116
113
|
*
|
|
117
114
|
* Line order: total spend, one line per rate tier (aggregated across callers,
|
|
118
|
-
* sorted by spend descending),
|
|
119
|
-
* boosted rows, a live-toggle boost line (see below), the savings line (only
|
|
115
|
+
* sorted by spend descending), the savings line (only
|
|
120
116
|
* when a counterfactual total resolved), an incomplete-counterfactual note,
|
|
121
117
|
* an unbilled note, a dropped-run-ids note (carry-over from the /cost
|
|
122
118
|
* re-review — see sessionRuns.ts's `droppedSessionRuns`), then headroom.
|
|
123
119
|
*
|
|
124
120
|
* `droppedRunCount` defaults to 0 (no note) so every existing direct caller
|
|
125
121
|
* of this pure function keeps behaving identically without passing it.
|
|
126
|
-
*
|
|
127
|
-
* `boosted` (default false) is the LIVE client-side toggle from
|
|
128
|
-
* `boostCommand.ts`'s `isBoosted()`, threaded in exactly like `advisorLine`
|
|
129
|
-
* is for {@link formatCostLine}. It is independent of the per-tier "Boosted
|
|
130
|
-
* (tier) spend" subtotal above: that subtotal only reflects server rows
|
|
131
|
-
* carrying `x-yagni-boost`, which only `/go` children (and other spawned
|
|
132
|
-
* processes) ever send — the driver's own turns never do (see
|
|
133
|
-
* boostCommand.ts's KNOWN-asymmetry docblock). Without this flag, a session
|
|
134
|
-
* that only chats while boosted would show no boost line at all.
|
|
135
122
|
*/
|
|
136
|
-
export function formatServerCostLines(spend, headroom, droppedRunCount = 0
|
|
123
|
+
export function formatServerCostLines(spend, headroom, droppedRunCount = 0) {
|
|
137
124
|
const lines = [`Session spend: $${usd(spend.totalSellMillicents)} (server).`];
|
|
138
125
|
const tierTotals = new Map();
|
|
139
126
|
for (const row of spend.rows) {
|
|
@@ -147,23 +134,6 @@ export function formatServerCostLines(spend, headroom, droppedRunCount = 0, boos
|
|
|
147
134
|
const calls = `${fmt(agg.dispatches)} call${agg.dispatches === 1 ? "" : "s"}`;
|
|
148
135
|
lines.push(` ${tier}: $${usd(agg.sellMillicents)} over ${calls}.`);
|
|
149
136
|
}
|
|
150
|
-
const boostTotals = new Map();
|
|
151
|
-
for (const row of spend.rows) {
|
|
152
|
-
if (!row.boost)
|
|
153
|
-
continue;
|
|
154
|
-
boostTotals.set(row.rateTier, (boostTotals.get(row.rateTier) ?? 0) + row.sellMillicents);
|
|
155
|
-
}
|
|
156
|
-
if (boostTotals.size > 0) {
|
|
157
|
-
for (const [tier] of tiersSorted) {
|
|
158
|
-
const tierBoosted = boostTotals.get(tier);
|
|
159
|
-
if (tierBoosted === undefined)
|
|
160
|
-
continue;
|
|
161
|
-
lines.push(` Boosted (${tier}) spend: $${usd(tierBoosted)}.`);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
if (boosted) {
|
|
165
|
-
lines.push(BOOST_LINE);
|
|
166
|
-
}
|
|
167
137
|
// typeof guard (not just !== null): a network response is untyped at
|
|
168
138
|
// runtime, and a stray string/boolean here must never sneak "NN%" into the
|
|
169
139
|
// rendered line.
|
|
@@ -233,15 +203,6 @@ export function registerCostCommand(pi, deps = {}) {
|
|
|
233
203
|
: Promise.resolve(null),
|
|
234
204
|
]);
|
|
235
205
|
const localSnap = acc.snapshot();
|
|
236
|
-
// Read fresh on every /cost call, both branches (see RegisterCostDeps's
|
|
237
|
-
// isBoosted doc comment for why the local fallback needs it too).
|
|
238
|
-
let boosted = false;
|
|
239
|
-
try {
|
|
240
|
-
boosted = deps.isBoosted?.() ?? false;
|
|
241
|
-
}
|
|
242
|
-
catch {
|
|
243
|
-
/* a diagnostic must never break /cost */
|
|
244
|
-
}
|
|
245
206
|
// Quiet divergence check: both totals must be available, and it never
|
|
246
207
|
// affects what the user sees. Scoped to `caller === "driver"` rows only
|
|
247
208
|
// (like-for-like with the local `turn_end` accumulator, which can only
|
|
@@ -276,7 +237,7 @@ export function registerCostCommand(pi, deps = {}) {
|
|
|
276
237
|
catch {
|
|
277
238
|
/* a diagnostic must never break /cost */
|
|
278
239
|
}
|
|
279
|
-
await pi.sendUserMessage(formatServerCostLines(spend, headroom, dropped
|
|
240
|
+
await pi.sendUserMessage(formatServerCostLines(spend, headroom, dropped));
|
|
280
241
|
return;
|
|
281
242
|
}
|
|
282
243
|
let advisorLine = "";
|
|
@@ -286,7 +247,7 @@ export function registerCostCommand(pi, deps = {}) {
|
|
|
286
247
|
catch {
|
|
287
248
|
/* usage accounting must never break /cost */
|
|
288
249
|
}
|
|
289
|
-
await pi.sendUserMessage(formatCostLine(localSnap, headroom, advisorLine, "local, driver only"
|
|
250
|
+
await pi.sendUserMessage(formatCostLine(localSnap, headroom, advisorLine, "local, driver only"));
|
|
290
251
|
},
|
|
291
252
|
});
|
|
292
253
|
}
|
|
@@ -125,8 +125,6 @@ export function makeCrashReporter(opts) {
|
|
|
125
125
|
if (crashReportsDisabled(env))
|
|
126
126
|
return;
|
|
127
127
|
const token = opts.getToken();
|
|
128
|
-
if (!token)
|
|
129
|
-
return;
|
|
130
128
|
const sanitized = sanitizeCrashError(error, { env, repoRoot });
|
|
131
129
|
const payload = {
|
|
132
130
|
client: isDesktopSurface() ? "desktop" : "cli",
|
|
@@ -148,7 +146,7 @@ export function makeCrashReporter(opts) {
|
|
|
148
146
|
method: "POST",
|
|
149
147
|
headers: {
|
|
150
148
|
"content-type": "application/json",
|
|
151
|
-
authorization: `Bearer ${token}
|
|
149
|
+
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
152
150
|
},
|
|
153
151
|
body: JSON.stringify(payload),
|
|
154
152
|
signal: controller.signal,
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exec policy engine — classifies bash commands via prefix rules + lightweight
|
|
3
|
+
* shell tokenization (YAG-504, restructured in YAG-510).
|
|
4
|
+
*
|
|
5
|
+
* Pure: no I/O, no network, no model. Loads at startup and classifies
|
|
6
|
+
* synchronously. The curated default set auto-allows read-only commands
|
|
7
|
+
* (ls, cat, rg, git status/log/diff), forbids destructive ones (rm -rf,
|
|
8
|
+
* git reset --hard, git push --force, pipe-to-shell), and prompts for the
|
|
9
|
+
* ambiguous middle band (npm install, git commit, curl, …).
|
|
10
|
+
*
|
|
11
|
+
* The `prompt` band is what the Guardian arbitrates — see guardian.ts.
|
|
12
|
+
*
|
|
13
|
+
* Classification composes three signals and takes the STRICTEST:
|
|
14
|
+
* 1. prefix-rule matching on every segment (newlines, ;, &&, ||, | split);
|
|
15
|
+
* 2. a construct floor — commands using redirects, substitution, or
|
|
16
|
+
* background & can never be auto-allowed (floor: prompt);
|
|
17
|
+
* 3. dangerScan — a best-effort sweep of command-substitution inner text
|
|
18
|
+
* ($(...) and backticks, including inside double quotes) against the
|
|
19
|
+
* FORBIDDEN rules only. Danger anywhere upgrades to forbidden; the scan
|
|
20
|
+
* can never make anything more permissive.
|
|
21
|
+
* This is the codex two-parser lesson: fail closed to prove safety, scan
|
|
22
|
+
* best-effort to prove danger. A forbidden match must win even when the
|
|
23
|
+
* command also carries constructs (`rm -rf / &` is forbidden, not prompt).
|
|
24
|
+
*
|
|
25
|
+
* Command words are matched through a leading-token strip (env assignments,
|
|
26
|
+
* sudo/env/command wrappers, shell reserved words, a leading backslash) and
|
|
27
|
+
* basename normalization (/bin/rm → rm) — both applied ASYMMETRICALLY: they
|
|
28
|
+
* can make a command land on forbidden/prompt rules, but a stripped or
|
|
29
|
+
* path-prefixed command is never auto-allowed (`sudo ls` and `./ls` stay in
|
|
30
|
+
* the prompt band; an attacker-named local `./rm` binary must not ride the
|
|
31
|
+
* allow list, and `/bin/ls` pays the same price by design).
|
|
32
|
+
*
|
|
33
|
+
* Tokenization is a lightweight inline parser — not shell-quote — because the
|
|
34
|
+
* extension is bundled into @yagni-app/code's dist (a file copy, not a real
|
|
35
|
+
* bundler), and external dependencies aren't resolvable from the bundled path.
|
|
36
|
+
*/
|
|
37
|
+
export type TokenEntry = string | {
|
|
38
|
+
op: "pipe" | "and" | "or" | "semi" | "substitution";
|
|
39
|
+
} | {
|
|
40
|
+
op: "redirect";
|
|
41
|
+
direction: "out";
|
|
42
|
+
fd: "stdout" | "stderr";
|
|
43
|
+
target: string;
|
|
44
|
+
append: boolean;
|
|
45
|
+
} | {
|
|
46
|
+
op: "redirect";
|
|
47
|
+
direction: "in";
|
|
48
|
+
} | {
|
|
49
|
+
op: "background";
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Parse a shell command string into tokens and control operators.
|
|
53
|
+
*
|
|
54
|
+
* Handles:
|
|
55
|
+
* - Single and double quoted strings (preserves spaces inside)
|
|
56
|
+
* - Control operators: |, &&, ||, ;, and newlines (a newline separates
|
|
57
|
+
* commands exactly like `;` — treating it as whitespace let multiline
|
|
58
|
+
* commands smuggle anything behind an allow-listed first line)
|
|
59
|
+
* - `#` comments (start-of-word to end-of-line, outside quotes)
|
|
60
|
+
* - Shell constructs we flag as unanalyzable: $(), backticks (INCLUDING
|
|
61
|
+
* inside double quotes — bash executes those), >, <, background &
|
|
62
|
+
* - Redirect metadata: stdout/stderr redirects carry fd + target so that
|
|
63
|
+
* safe redirects (2>/dev/null, 2>&1) can be distinguished from unsafe ones
|
|
64
|
+
* (> file.txt). Stdin redirects (<, <<) carry no metadata — they always
|
|
65
|
+
* floor. Background & emits a distinct `background` op (not `semi`) so
|
|
66
|
+
* hasUnhandledConstructs can always catch it.
|
|
67
|
+
*
|
|
68
|
+
* Does NOT handle: variable expansion, glob patterns, heredocs beyond the
|
|
69
|
+
* redirect flag, nested subshells beyond depth tracking. Commands using
|
|
70
|
+
* those are classified as "prompt" at minimum (construct floor).
|
|
71
|
+
*/
|
|
72
|
+
export declare function shellParse(command: string): TokenEntry[];
|
|
73
|
+
/**
|
|
74
|
+
* Extract the inner text of every command substitution — $(...) and
|
|
75
|
+
* backticks — respecting single-quote literalness and backslash escapes.
|
|
76
|
+
* Includes substitutions inside double quotes (bash executes those).
|
|
77
|
+
* Best-effort, used ONLY by dangerScan to prove danger, never safety.
|
|
78
|
+
*/
|
|
79
|
+
export declare function extractSubstitutions(command: string): string[];
|
|
80
|
+
export type ExecDecision = "allow" | "prompt" | "forbidden";
|
|
81
|
+
export interface PrefixRule {
|
|
82
|
+
/** Ordered tokens; a string[] element means alternatives (any match). */
|
|
83
|
+
pattern: (string | string[])[];
|
|
84
|
+
decision: ExecDecision;
|
|
85
|
+
justification: string;
|
|
86
|
+
/**
|
|
87
|
+
* Escape hatch for allow rules whose command has a mutating flag: if any
|
|
88
|
+
* token AFTER the matched prefix equals one of these (or, for entries ending
|
|
89
|
+
* in "*", starts with the part before the star), the rule does NOT match and
|
|
90
|
+
* evaluation falls through to later rules (usually landing in the prompt
|
|
91
|
+
* band). Example: sed is read-only except with -i/--in-place.
|
|
92
|
+
*/
|
|
93
|
+
unlessTokens?: string[];
|
|
94
|
+
/**
|
|
95
|
+
* Position-independent flag requirement: the rule matches only when, in
|
|
96
|
+
* addition to the pattern prefix, at least one token AFTER the prefix
|
|
97
|
+
* matches an entry (same "*"-suffix glob convention as unlessTokens).
|
|
98
|
+
* Used by forbidden rules to catch permuted flags: `git push origin
|
|
99
|
+
* --force` and `rm x -rf` place the dangerous flag after positional args,
|
|
100
|
+
* where exact-position patterns never see it.
|
|
101
|
+
*/
|
|
102
|
+
flagsAnywhere?: string[];
|
|
103
|
+
/** Positive test invocations (validated at load if present). */
|
|
104
|
+
match?: string[][];
|
|
105
|
+
/** Negative test invocations (validated at load if present). */
|
|
106
|
+
notMatch?: string[][];
|
|
107
|
+
}
|
|
108
|
+
export interface ExecPolicy {
|
|
109
|
+
rules: PrefixRule[];
|
|
110
|
+
}
|
|
111
|
+
export interface ExecClassification {
|
|
112
|
+
decision: ExecDecision;
|
|
113
|
+
justification: string;
|
|
114
|
+
matchedRule?: PrefixRule;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Parse a command string into tokens using our lightweight tokenizer. Returns
|
|
118
|
+
* string tokens only (control operators and constructs are filtered out —
|
|
119
|
+
* detected separately).
|
|
120
|
+
*/
|
|
121
|
+
export declare function tokenize(command: string): string[];
|
|
122
|
+
/**
|
|
123
|
+
* Classify a full bash command string against the exec policy.
|
|
124
|
+
*
|
|
125
|
+
* Compound commands (pipes, &&, ||, ;, newlines) are split into segments and
|
|
126
|
+
* each is classified independently; the strictest decision wins (forbidden >
|
|
127
|
+
* prompt > allow). Commands with shell constructs (substitution, redirects,
|
|
128
|
+
* background &) have a floor of `prompt`, and their substitution inner text
|
|
129
|
+
* is danger-scanned against the forbidden rules. Pipe-to-shell is always
|
|
130
|
+
* forbidden.
|
|
131
|
+
*/
|
|
132
|
+
export declare function classifyCommand(command: string, policy: ExecPolicy): ExecClassification;
|
|
133
|
+
/** Curated default rules — the shipped safety floor. */
|
|
134
|
+
export declare const DEFAULT_EXEC_POLICY: ExecPolicy;
|
|
135
|
+
//# sourceMappingURL=execPolicy.d.ts.map
|