@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
package/dist/cli.d.ts
CHANGED
|
@@ -12,6 +12,36 @@
|
|
|
12
12
|
* plus a configured pi spawn. Everything that makes this "YAGNI Code" lives in
|
|
13
13
|
* pi-extension-yagni and the YAGNI backend.
|
|
14
14
|
*/
|
|
15
|
+
/**
|
|
16
|
+
* Seed `editorPaddingX` into the per-profile pi `settings.json` so the prompt
|
|
17
|
+
* input aligns with the chat/output area (`outputPad`) and the status bar.
|
|
18
|
+
* Fills the key in only when absent — an explicit user choice (including 0)
|
|
19
|
+
* is never overwritten. Best-effort: a missing/corrupt settings file or a
|
|
20
|
+
* write failure must never block the launch.
|
|
21
|
+
*/
|
|
22
|
+
export declare function seedEditorPadding(piAgentDir: string): void;
|
|
23
|
+
/**
|
|
24
|
+
* Seed `collapseChangelog` into the per-profile pi `settings.json` so the
|
|
25
|
+
* "What's New" section on startup renders as a one-liner instead of a full
|
|
26
|
+
* markdown dump. Fills the key in only when absent — an explicit user choice
|
|
27
|
+
* (including false) is never overwritten. Best-effort: a missing/corrupt
|
|
28
|
+
* settings file or a write failure must never block the launch.
|
|
29
|
+
*/
|
|
30
|
+
export declare function seedCollapseChangelog(piAgentDir: string): void;
|
|
31
|
+
/**
|
|
32
|
+
* Seed `hideThinkingBlock` into the per-profile pi `settings.json` so the
|
|
33
|
+
* model's raw chain-of-thought collapses to a single labeled line instead of
|
|
34
|
+
* streaming its entire reasoning to the terminal. Fills the key in only when
|
|
35
|
+
* absent — an explicit user choice (including false, e.g. via Ctrl+T) is never
|
|
36
|
+
* overwritten. Best-effort: a missing/corrupt settings file or a write failure
|
|
37
|
+
* must never block the launch.
|
|
38
|
+
*
|
|
39
|
+
* Returns true only when this call actually collapsed reasoning for the first
|
|
40
|
+
* time (the key was absent and the write succeeded), so the caller can pass a
|
|
41
|
+
* one-time hint to the extension. Ctrl+T still reveals the full trace and
|
|
42
|
+
* persists the user's choice, so this default is always reversible.
|
|
43
|
+
*/
|
|
44
|
+
export declare function seedHideThinkingBlock(piAgentDir: string): boolean;
|
|
15
45
|
export declare const HELP_TEXT: string;
|
|
16
46
|
/** Parse `use <name> [--base-url <url>]` argv into its parts. */
|
|
17
47
|
export declare function parseUseArgs(args: string[]): {
|
package/dist/cli.js
CHANGED
|
@@ -13,20 +13,25 @@
|
|
|
13
13
|
* pi-extension-yagni and the YAGNI backend.
|
|
14
14
|
*/
|
|
15
15
|
import { spawn } from "node:child_process";
|
|
16
|
-
import { mkdirSync, realpathSync } from "node:fs";
|
|
16
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
17
18
|
import { createInterface } from "node:readline/promises";
|
|
18
19
|
import { fileURLToPath } from "node:url";
|
|
19
20
|
import { PI_CONFIG_NAME } from "./branding.js";
|
|
20
21
|
import { claudeCompatArgs } from "./claudeCompat.js";
|
|
21
22
|
import { agentDir, credentialsDir, piPackageDir } from "./credentials.js";
|
|
22
23
|
import { DISTRIBUTION } from "./distribution.js";
|
|
24
|
+
import { connectCommand } from "./connectClaudeCode.js";
|
|
23
25
|
import { login } from "./login.js";
|
|
24
26
|
import { logout } from "./logout.js";
|
|
27
|
+
import { tokenCommand } from "./token.js";
|
|
25
28
|
import { buildLaunch } from "./launch.js";
|
|
26
29
|
import { runDoctor } from "./doctor.js";
|
|
27
30
|
import { installProcessCrashHandlers } from "./crashReport.js";
|
|
28
31
|
import { currentCliVersion, maybeNudgeAndRefresh, upgradeCommand } from "./upgrade.js";
|
|
29
32
|
import { maybeRefreshAtLaunch } from "./refresh.js";
|
|
33
|
+
import { exitCodeFor, installSignalForwarding } from "./signalForward.js";
|
|
34
|
+
import { PAD_X } from "./padding.js";
|
|
30
35
|
import { ensureShadowPiPackage } from "./piPackage.js";
|
|
31
36
|
import { resolveExtensionPath, resolvePiCliPath, resolvePiPackageDir } from "./paths.js";
|
|
32
37
|
import { credentialsFromProfile, getActiveProfileName, listProfiles, migrateLegacyCredentials, persistProfileTokenRotation, profilePath, readActiveProfile, useProfile, } from "./profiles.js";
|
|
@@ -43,6 +48,115 @@ async function confirmOnTty(question) {
|
|
|
43
48
|
rl.close();
|
|
44
49
|
}
|
|
45
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Seed `editorPaddingX` into the per-profile pi `settings.json` so the prompt
|
|
53
|
+
* input is padded to match the chat/output area (`outputPad`) and the status
|
|
54
|
+
* bar. Fills the key in only when absent — an explicit user choice (including
|
|
55
|
+
* 0) is never overwritten. Best-effort: a missing/corrupt settings file or a
|
|
56
|
+
* write failure must never block the launch.
|
|
57
|
+
*/
|
|
58
|
+
/**
|
|
59
|
+
* Seed a key into the per-profile pi `settings.json`. Shared guard rails for
|
|
60
|
+
* all settings seeds:
|
|
61
|
+
*
|
|
62
|
+
* This writes to a user-owned file, so it is deliberately conservative:
|
|
63
|
+
* - fills the key in ONLY when absent — an explicit user choice (including 0
|
|
64
|
+
* or false) is never overwritten;
|
|
65
|
+
* - backs off on a missing-dir, corrupt, non-object, or symlinked settings
|
|
66
|
+
* file rather than risk clobbering anything;
|
|
67
|
+
* - writes ATOMICALLY (temp file + rename) and preserves the existing file's
|
|
68
|
+
* permissions, so a crash mid-write can never truncate the user's settings.
|
|
69
|
+
* Any failure is swallowed: seeding must never block or break a launch.
|
|
70
|
+
*
|
|
71
|
+
* Returns true only when this call actually wrote the key (the key was absent
|
|
72
|
+
* and the write succeeded), letting callers distinguish a first-seed from a
|
|
73
|
+
* no-op so they can surface a one-time hint. Returns false on any existing
|
|
74
|
+
* value (explicit or previously seeded), a back-off case, or a write failure.
|
|
75
|
+
*/
|
|
76
|
+
function seedSetting(piAgentDir, key, value) {
|
|
77
|
+
try {
|
|
78
|
+
const settingsPath = join(piAgentDir, "settings.json");
|
|
79
|
+
let settings = {};
|
|
80
|
+
let existingMode;
|
|
81
|
+
if (existsSync(settingsPath)) {
|
|
82
|
+
// Refuse to follow a symlink: we must only ever write a regular, real
|
|
83
|
+
// settings file the user (or pi) owns.
|
|
84
|
+
if (lstatSync(settingsPath).isSymbolicLink())
|
|
85
|
+
return false;
|
|
86
|
+
let parsed;
|
|
87
|
+
try {
|
|
88
|
+
parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return false; // corrupt file: back off rather than clobber the user's settings
|
|
92
|
+
}
|
|
93
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
94
|
+
return false; // non-object settings: leave it alone
|
|
95
|
+
}
|
|
96
|
+
settings = parsed;
|
|
97
|
+
existingMode = lstatSync(settingsPath).mode & 0o777;
|
|
98
|
+
}
|
|
99
|
+
if (settings[key] !== undefined)
|
|
100
|
+
return false; // user (or a prior seed) already set it
|
|
101
|
+
settings[key] = value;
|
|
102
|
+
// Atomic write: serialize to a temp file in the same directory, then
|
|
103
|
+
// rename over the target. A crash leaves either the old file or the temp
|
|
104
|
+
// file, never a half-written settings.json.
|
|
105
|
+
const tmpPath = join(piAgentDir, `.settings.json.yagni-${process.pid}-${Date.now()}.tmp`);
|
|
106
|
+
try {
|
|
107
|
+
writeFileSync(tmpPath, `${JSON.stringify(settings, null, 2)}\n`, { mode: 0o600 });
|
|
108
|
+
renameSync(tmpPath, settingsPath);
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
rmSync(tmpPath, { force: true }); // no-op once the rename succeeded
|
|
112
|
+
}
|
|
113
|
+
// Restore the original permissions if the file already had them (rename
|
|
114
|
+
// replaces the inode, which would otherwise reset to 0600).
|
|
115
|
+
if (existingMode !== undefined)
|
|
116
|
+
chmodSync(settingsPath, existingMode);
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// never block launch on a settings-seed failure
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Seed `editorPaddingX` into the per-profile pi `settings.json` so the prompt
|
|
126
|
+
* input aligns with the chat/output area (`outputPad`) and the status bar.
|
|
127
|
+
* Fills the key in only when absent — an explicit user choice (including 0)
|
|
128
|
+
* is never overwritten. Best-effort: a missing/corrupt settings file or a
|
|
129
|
+
* write failure must never block the launch.
|
|
130
|
+
*/
|
|
131
|
+
export function seedEditorPadding(piAgentDir) {
|
|
132
|
+
seedSetting(piAgentDir, "editorPaddingX", PAD_X);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Seed `collapseChangelog` into the per-profile pi `settings.json` so the
|
|
136
|
+
* "What's New" section on startup renders as a one-liner instead of a full
|
|
137
|
+
* markdown dump. Fills the key in only when absent — an explicit user choice
|
|
138
|
+
* (including false) is never overwritten. Best-effort: a missing/corrupt
|
|
139
|
+
* settings file or a write failure must never block the launch.
|
|
140
|
+
*/
|
|
141
|
+
export function seedCollapseChangelog(piAgentDir) {
|
|
142
|
+
seedSetting(piAgentDir, "collapseChangelog", true);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Seed `hideThinkingBlock` into the per-profile pi `settings.json` so the
|
|
146
|
+
* model's raw chain-of-thought collapses to a single labeled line instead of
|
|
147
|
+
* streaming its entire reasoning to the terminal. Fills the key in only when
|
|
148
|
+
* absent — an explicit user choice (including false, e.g. via Ctrl+T) is never
|
|
149
|
+
* overwritten. Best-effort: a missing/corrupt settings file or a write failure
|
|
150
|
+
* must never block the launch.
|
|
151
|
+
*
|
|
152
|
+
* Returns true only when this call actually collapsed reasoning for the first
|
|
153
|
+
* time (the key was absent and the write succeeded), so the caller can pass a
|
|
154
|
+
* one-time hint to the extension. Ctrl+T still reveals the full trace and
|
|
155
|
+
* persists the user's choice, so this default is always reversible.
|
|
156
|
+
*/
|
|
157
|
+
export function seedHideThinkingBlock(piAgentDir) {
|
|
158
|
+
return seedSetting(piAgentDir, "hideThinkingBlock", true);
|
|
159
|
+
}
|
|
46
160
|
async function runDefault(passthroughArgs) {
|
|
47
161
|
// Cache-backed update nudge (never a network wait), then a background cache
|
|
48
162
|
// refresh that completes while the session runs. Both fail soft.
|
|
@@ -72,6 +186,20 @@ async function runDefault(passthroughArgs) {
|
|
|
72
186
|
// instead of ~/.pi/agent, and prod state never bleeds into staging.
|
|
73
187
|
const piAgentDir = agentDir(profile.name);
|
|
74
188
|
mkdirSync(piAgentDir, { recursive: true, mode: 0o700 });
|
|
189
|
+
// Seed the editor's horizontal padding so the prompt input aligns with the
|
|
190
|
+
// chat/output area and the status bar, and collapse the changelog so the
|
|
191
|
+
// "What's New" section on startup renders as a one-liner instead of a full
|
|
192
|
+
// markdown dump. Only fills in when the user has not set either themselves —
|
|
193
|
+
// an explicit choice is never clobbered. Best-effort: a corrupt or
|
|
194
|
+
// unreadable settings.json must never block the launch.
|
|
195
|
+
seedEditorPadding(piAgentDir);
|
|
196
|
+
seedCollapseChangelog(piAgentDir);
|
|
197
|
+
// Collapse raw chain-of-thought to one line on first launch. Seeded here
|
|
198
|
+
// (before the extension boots) so `setHiddenThinkingLabel`/any Ctrl+T toggle
|
|
199
|
+
// in this session sees the collapsed default. `seededHideThinking` is true
|
|
200
|
+
// only THIS launch — a downstream one-time hint must not re-nag on the next
|
|
201
|
+
// real run once the key already exists.
|
|
202
|
+
const seededHideThinking = seedHideThinkingBlock(piAgentDir);
|
|
75
203
|
// Generate the shadow pi package so the terminal title/process name read
|
|
76
204
|
// "YAGNI Code" instead of "pi"/"π". Best-effort: if it can't be built we
|
|
77
205
|
// still launch (un-rebranded but hermetic), never blocking the agent.
|
|
@@ -120,6 +248,7 @@ async function runDefault(passthroughArgs) {
|
|
|
120
248
|
stateDir: credentialsDir(),
|
|
121
249
|
cliVersion: cliVersion(),
|
|
122
250
|
baseEnv: process.env,
|
|
251
|
+
...(seededHideThinking ? { hideThinkingSeeded: true } : {}),
|
|
123
252
|
});
|
|
124
253
|
}
|
|
125
254
|
catch (err) {
|
|
@@ -136,7 +265,12 @@ async function runDefault(passthroughArgs) {
|
|
|
136
265
|
stdio: "inherit",
|
|
137
266
|
env,
|
|
138
267
|
});
|
|
139
|
-
|
|
268
|
+
// Forward termination signals to pi instead of dying around it (see
|
|
269
|
+
// signalForward.ts for the policy: first signal graceful, second tree-kill).
|
|
270
|
+
installSignalForwarding(child);
|
|
271
|
+
// 128+n for a signal death (bash parity), so a cancelled/killed run never
|
|
272
|
+
// reads as success to scripts or CI.
|
|
273
|
+
child.on("exit", (code, signal) => resolve(exitCodeFor(code, signal)));
|
|
140
274
|
child.on("error", (err) => {
|
|
141
275
|
process.stderr.write(`Failed to start YAGNI Code: ${err.message}\n`);
|
|
142
276
|
resolve(1);
|
|
@@ -154,6 +288,10 @@ export const HELP_TEXT = [
|
|
|
154
288
|
" yagni login Authorize the active environment (device-code flow).",
|
|
155
289
|
" yagni logout Revoke and clear the active environment's token.",
|
|
156
290
|
" yagni doctor Check that everything is ready (green/red checklist).",
|
|
291
|
+
" yagni connect claude-code Route Claude Code through the YAGNI model proxy",
|
|
292
|
+
" (--project scopes to this repo; --off disconnects).",
|
|
293
|
+
" yagni connect codex Route Codex CLI through the YAGNI model proxy.",
|
|
294
|
+
" yagni token Output the active environment's API token (for helpers).",
|
|
157
295
|
" yagni use <name> Switch the active environment (sticky).",
|
|
158
296
|
" Presets: prod, local. Others need --base-url <url>.",
|
|
159
297
|
" yagni profiles List saved environments; the active one is marked.",
|
|
@@ -161,7 +299,7 @@ export const HELP_TEXT = [
|
|
|
161
299
|
" yagni version Print the CLI version.",
|
|
162
300
|
"",
|
|
163
301
|
"Common agent flags (passed straight through):",
|
|
164
|
-
" --model <tier>
|
|
302
|
+
" --model <tier> Model tier (fixed to advanced).",
|
|
165
303
|
" --thinking <level> off | minimal | low | medium | high | xhigh | max",
|
|
166
304
|
" --session <id> Open a specific session; --fork <id> branches one.",
|
|
167
305
|
" --mode json Emit machine-readable events (for scripts and CI).",
|
|
@@ -283,6 +421,12 @@ export async function main(argv) {
|
|
|
283
421
|
if (command === "doctor") {
|
|
284
422
|
return runDoctor();
|
|
285
423
|
}
|
|
424
|
+
if (command === "connect") {
|
|
425
|
+
return connectCommand(rest);
|
|
426
|
+
}
|
|
427
|
+
if (command === "token") {
|
|
428
|
+
return tokenCommand();
|
|
429
|
+
}
|
|
286
430
|
if (command === "upgrade") {
|
|
287
431
|
return upgradeCommand(rest, { current: cliVersion() });
|
|
288
432
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `yagni connect claude-code` — point Claude Code at the YAGNI model proxy.
|
|
3
|
+
*
|
|
4
|
+
* Writes the three settings Claude Code needs into `~/.claude/settings.json`
|
|
5
|
+
* (or `.claude/settings.local.json` with `--project`):
|
|
6
|
+
*
|
|
7
|
+
* env.ANTHROPIC_BASE_URL the active environment's base URL (Claude Code
|
|
8
|
+
* appends /v1/messages — the proxy's Anthropic-
|
|
9
|
+
* dialect route)
|
|
10
|
+
* env.ANTHROPIC_CUSTOM_HEADERS `x-yagni-caller: claude-code`, the
|
|
11
|
+
* attribution label the usage report rolls up on
|
|
12
|
+
* apiKeyHelper `yagni token` — the credential comes from the
|
|
13
|
+
* profile at call time, so nothing secret is
|
|
14
|
+
* ever baked into a settings file and a rotated
|
|
15
|
+
* token is picked up automatically
|
|
16
|
+
*
|
|
17
|
+
* Everything else in the file is preserved verbatim. `--off` removes exactly
|
|
18
|
+
* the managed keys (the helper only when it is ours). The planner is pure and
|
|
19
|
+
* the writer is atomic (temp + rename, symlink-refusing) — the same guard
|
|
20
|
+
* rails as the launcher's settings seeding, except a corrupt file is an ERROR
|
|
21
|
+
* here rather than a silent back-off: the user asked for a config change, so
|
|
22
|
+
* failing quietly would be lying.
|
|
23
|
+
*/
|
|
24
|
+
import { type Profile } from "./profiles.js";
|
|
25
|
+
export declare const CALLER_HEADER_LINE = "x-yagni-caller: claude-code";
|
|
26
|
+
export interface ConnectPlanInput {
|
|
27
|
+
/** The active environment's base URL (no trailing slash). */
|
|
28
|
+
baseUrl: string;
|
|
29
|
+
/** The apiKeyHelper command, e.g. `yagni token`. */
|
|
30
|
+
helperCommand: string;
|
|
31
|
+
}
|
|
32
|
+
export interface ConnectPlan {
|
|
33
|
+
settings: Record<string, unknown>;
|
|
34
|
+
/** Human-readable `key: old → new` lines for the summary. */
|
|
35
|
+
changes: string[];
|
|
36
|
+
/** Keys that belonged to something else and were replaced — surfaced loudly. */
|
|
37
|
+
replaced: string[];
|
|
38
|
+
}
|
|
39
|
+
type Settings = Record<string, unknown>;
|
|
40
|
+
/**
|
|
41
|
+
* Merge our caller header into an existing ANTHROPIC_CUSTOM_HEADERS value:
|
|
42
|
+
* foreign header lines are preserved, a stale x-yagni-caller line is replaced,
|
|
43
|
+
* ours lands last. Newline-separated per the Claude Code contract.
|
|
44
|
+
*/
|
|
45
|
+
export declare function mergeCustomHeaders(existing: unknown): string;
|
|
46
|
+
/** Pure: the settings object after connecting, plus what changed. */
|
|
47
|
+
export declare function planConnect(existing: Settings, input: ConnectPlanInput): ConnectPlan;
|
|
48
|
+
export interface DisconnectPlan {
|
|
49
|
+
settings: Settings;
|
|
50
|
+
removed: string[];
|
|
51
|
+
/** A foreign apiKeyHelper we refused to touch, if any. */
|
|
52
|
+
keptForeignHelper?: string;
|
|
53
|
+
}
|
|
54
|
+
/** Pure: the settings object after `--off` — managed keys out, all else kept. */
|
|
55
|
+
export declare function planDisconnect(existing: Settings, helperCommand: string): DisconnectPlan;
|
|
56
|
+
export declare function settingsPathFor(scope: "user" | "project", cwd: string, home?: string): string;
|
|
57
|
+
/** Read + parse a settings file. Missing → {}. Corrupt/symlink/non-object → throws. */
|
|
58
|
+
export declare function readSettings(path: string): Settings;
|
|
59
|
+
/** Atomic write (temp + rename), preserving an existing file's permissions. */
|
|
60
|
+
export declare function writeSettings(path: string, settings: Settings): void;
|
|
61
|
+
export interface ConnectArgs {
|
|
62
|
+
target?: string;
|
|
63
|
+
project: boolean;
|
|
64
|
+
off: boolean;
|
|
65
|
+
}
|
|
66
|
+
export declare function parseConnectArgs(args: string[]): ConnectArgs;
|
|
67
|
+
export interface ConnectDeps {
|
|
68
|
+
readProfile?: () => Promise<Profile>;
|
|
69
|
+
cwd?: string;
|
|
70
|
+
home?: string;
|
|
71
|
+
env?: NodeJS.ProcessEnv;
|
|
72
|
+
stdout?: (text: string) => void;
|
|
73
|
+
stderr?: (text: string) => void;
|
|
74
|
+
}
|
|
75
|
+
export declare function connectCommand(args: string[], deps?: ConnectDeps): Promise<number>;
|
|
76
|
+
export {};
|
|
77
|
+
//# sourceMappingURL=connectClaudeCode.d.ts.map
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `yagni connect claude-code` — point Claude Code at the YAGNI model proxy.
|
|
3
|
+
*
|
|
4
|
+
* Writes the three settings Claude Code needs into `~/.claude/settings.json`
|
|
5
|
+
* (or `.claude/settings.local.json` with `--project`):
|
|
6
|
+
*
|
|
7
|
+
* env.ANTHROPIC_BASE_URL the active environment's base URL (Claude Code
|
|
8
|
+
* appends /v1/messages — the proxy's Anthropic-
|
|
9
|
+
* dialect route)
|
|
10
|
+
* env.ANTHROPIC_CUSTOM_HEADERS `x-yagni-caller: claude-code`, the
|
|
11
|
+
* attribution label the usage report rolls up on
|
|
12
|
+
* apiKeyHelper `yagni token` — the credential comes from the
|
|
13
|
+
* profile at call time, so nothing secret is
|
|
14
|
+
* ever baked into a settings file and a rotated
|
|
15
|
+
* token is picked up automatically
|
|
16
|
+
*
|
|
17
|
+
* Everything else in the file is preserved verbatim. `--off` removes exactly
|
|
18
|
+
* the managed keys (the helper only when it is ours). The planner is pure and
|
|
19
|
+
* the writer is atomic (temp + rename, symlink-refusing) — the same guard
|
|
20
|
+
* rails as the launcher's settings seeding, except a corrupt file is an ERROR
|
|
21
|
+
* here rather than a silent back-off: the user asked for a config change, so
|
|
22
|
+
* failing quietly would be lying.
|
|
23
|
+
*/
|
|
24
|
+
import { existsSync, lstatSync, readFileSync, renameSync, rmSync, writeFileSync, mkdirSync, chmodSync } from "node:fs";
|
|
25
|
+
import { homedir } from "node:os";
|
|
26
|
+
import { dirname, join } from "node:path";
|
|
27
|
+
import { DISTRIBUTION } from "./distribution.js";
|
|
28
|
+
import { credentialsFromProfile, readActiveProfile } from "./profiles.js";
|
|
29
|
+
export const CALLER_HEADER_LINE = "x-yagni-caller: claude-code";
|
|
30
|
+
function envOf(settings) {
|
|
31
|
+
const env = settings.env;
|
|
32
|
+
return env && typeof env === "object" && !Array.isArray(env) ? { ...env } : {};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Merge our caller header into an existing ANTHROPIC_CUSTOM_HEADERS value:
|
|
36
|
+
* foreign header lines are preserved, a stale x-yagni-caller line is replaced,
|
|
37
|
+
* ours lands last. Newline-separated per the Claude Code contract.
|
|
38
|
+
*/
|
|
39
|
+
export function mergeCustomHeaders(existing) {
|
|
40
|
+
const lines = typeof existing === "string" && existing.length > 0 ? existing.split("\n") : [];
|
|
41
|
+
const kept = lines.filter((l) => l.trim() !== "" && !/^x-yagni-caller\s*:/i.test(l));
|
|
42
|
+
return [...kept, CALLER_HEADER_LINE].join("\n");
|
|
43
|
+
}
|
|
44
|
+
/** Pure: the settings object after connecting, plus what changed. */
|
|
45
|
+
export function planConnect(existing, input) {
|
|
46
|
+
const env = envOf(existing);
|
|
47
|
+
const changes = [];
|
|
48
|
+
const replaced = [];
|
|
49
|
+
const note = (key, prior, next) => {
|
|
50
|
+
if (prior === next)
|
|
51
|
+
return;
|
|
52
|
+
changes.push(prior === undefined ? `${key} = ${next}` : `${key}: ${String(prior)} → ${next}`);
|
|
53
|
+
if (prior !== undefined)
|
|
54
|
+
replaced.push(key);
|
|
55
|
+
};
|
|
56
|
+
note("env.ANTHROPIC_BASE_URL", env.ANTHROPIC_BASE_URL, input.baseUrl);
|
|
57
|
+
env.ANTHROPIC_BASE_URL = input.baseUrl;
|
|
58
|
+
const headers = mergeCustomHeaders(env.ANTHROPIC_CUSTOM_HEADERS);
|
|
59
|
+
if (env.ANTHROPIC_CUSTOM_HEADERS !== headers) {
|
|
60
|
+
changes.push(`env.ANTHROPIC_CUSTOM_HEADERS = ${headers.replaceAll("\n", " | ")}`);
|
|
61
|
+
}
|
|
62
|
+
env.ANTHROPIC_CUSTOM_HEADERS = headers;
|
|
63
|
+
note("apiKeyHelper", existing.apiKeyHelper, input.helperCommand);
|
|
64
|
+
return {
|
|
65
|
+
settings: { ...existing, env, apiKeyHelper: input.helperCommand },
|
|
66
|
+
changes,
|
|
67
|
+
replaced,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** Pure: the settings object after `--off` — managed keys out, all else kept. */
|
|
71
|
+
export function planDisconnect(existing, helperCommand) {
|
|
72
|
+
const env = envOf(existing);
|
|
73
|
+
const removed = [];
|
|
74
|
+
const out = { ...existing };
|
|
75
|
+
if (env.ANTHROPIC_BASE_URL !== undefined) {
|
|
76
|
+
removed.push("env.ANTHROPIC_BASE_URL");
|
|
77
|
+
delete env.ANTHROPIC_BASE_URL;
|
|
78
|
+
}
|
|
79
|
+
if (typeof env.ANTHROPIC_CUSTOM_HEADERS === "string") {
|
|
80
|
+
const kept = env.ANTHROPIC_CUSTOM_HEADERS
|
|
81
|
+
.split("\n")
|
|
82
|
+
.filter((l) => l.trim() !== "" && !/^x-yagni-caller\s*:/i.test(l));
|
|
83
|
+
if (kept.length !== env.ANTHROPIC_CUSTOM_HEADERS.split("\n").filter((l) => l.trim() !== "").length) {
|
|
84
|
+
removed.push("env.ANTHROPIC_CUSTOM_HEADERS (x-yagni-caller line)");
|
|
85
|
+
}
|
|
86
|
+
if (kept.length === 0)
|
|
87
|
+
delete env.ANTHROPIC_CUSTOM_HEADERS;
|
|
88
|
+
else
|
|
89
|
+
env.ANTHROPIC_CUSTOM_HEADERS = kept.join("\n");
|
|
90
|
+
}
|
|
91
|
+
let keptForeignHelper;
|
|
92
|
+
if (typeof out.apiKeyHelper === "string") {
|
|
93
|
+
// Only remove a helper that is ours — `yagni token` under any install path
|
|
94
|
+
// (npx wrapper, absolute bin). Someone else's gateway helper stays.
|
|
95
|
+
if (new RegExp(`(^|[/\\s])${helperCommand.split(" ")[0]}\\s+token\\s*$`).test(out.apiKeyHelper)) {
|
|
96
|
+
removed.push("apiKeyHelper");
|
|
97
|
+
delete out.apiKeyHelper;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
keptForeignHelper = out.apiKeyHelper;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (Object.keys(env).length === 0 && out.env !== undefined)
|
|
104
|
+
delete out.env;
|
|
105
|
+
else
|
|
106
|
+
out.env = env;
|
|
107
|
+
return { settings: out, removed, ...(keptForeignHelper ? { keptForeignHelper } : {}) };
|
|
108
|
+
}
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// File I/O
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
export function settingsPathFor(scope, cwd, home = homedir()) {
|
|
113
|
+
return scope === "project"
|
|
114
|
+
? join(cwd, ".claude", "settings.local.json")
|
|
115
|
+
: join(home, ".claude", "settings.json");
|
|
116
|
+
}
|
|
117
|
+
/** Read + parse a settings file. Missing → {}. Corrupt/symlink/non-object → throws. */
|
|
118
|
+
export function readSettings(path) {
|
|
119
|
+
if (!existsSync(path))
|
|
120
|
+
return {};
|
|
121
|
+
if (lstatSync(path).isSymbolicLink()) {
|
|
122
|
+
throw new Error(`${path} is a symlink; refusing to rewrite it. Point yagni at the real file.`);
|
|
123
|
+
}
|
|
124
|
+
let parsed;
|
|
125
|
+
try {
|
|
126
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
throw new Error(`${path} is not valid JSON. Fix or remove it, then re-run.`);
|
|
130
|
+
}
|
|
131
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
132
|
+
throw new Error(`${path} does not hold a JSON object. Fix or remove it, then re-run.`);
|
|
133
|
+
}
|
|
134
|
+
return parsed;
|
|
135
|
+
}
|
|
136
|
+
/** Atomic write (temp + rename), preserving an existing file's permissions. */
|
|
137
|
+
export function writeSettings(path, settings) {
|
|
138
|
+
const existingMode = existsSync(path) ? lstatSync(path).mode & 0o777 : undefined;
|
|
139
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
140
|
+
const tmp = join(dirname(path), `.${DISTRIBUTION.commandName}-connect-${process.pid}.tmp`);
|
|
141
|
+
try {
|
|
142
|
+
writeFileSync(tmp, `${JSON.stringify(settings, null, 2)}\n`);
|
|
143
|
+
renameSync(tmp, path);
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
rmSync(tmp, { force: true });
|
|
147
|
+
}
|
|
148
|
+
if (existingMode !== undefined)
|
|
149
|
+
chmodSync(path, existingMode);
|
|
150
|
+
}
|
|
151
|
+
export function parseConnectArgs(args) {
|
|
152
|
+
let target;
|
|
153
|
+
let project = false;
|
|
154
|
+
let off = false;
|
|
155
|
+
for (const a of args) {
|
|
156
|
+
if (a === "--project")
|
|
157
|
+
project = true;
|
|
158
|
+
else if (a === "--off")
|
|
159
|
+
off = true;
|
|
160
|
+
else if (!a.startsWith("-") && target === undefined)
|
|
161
|
+
target = a;
|
|
162
|
+
}
|
|
163
|
+
return { target, project, off };
|
|
164
|
+
}
|
|
165
|
+
export async function connectCommand(args, deps = {}) {
|
|
166
|
+
const stdout = deps.stdout ?? ((t) => process.stdout.write(t));
|
|
167
|
+
const stderr = deps.stderr ?? ((t) => process.stderr.write(t));
|
|
168
|
+
const { target, project, off } = parseConnectArgs(args);
|
|
169
|
+
if (target === "codex") {
|
|
170
|
+
if (project) {
|
|
171
|
+
stderr("`connect codex` has no --project scope: Codex reads one user-level config.toml.\n");
|
|
172
|
+
return 1;
|
|
173
|
+
}
|
|
174
|
+
const { connectCodexCommand } = await import("./connectCodex.js");
|
|
175
|
+
return connectCodexCommand({ off }, deps);
|
|
176
|
+
}
|
|
177
|
+
if (target !== "claude-code") {
|
|
178
|
+
stderr(target
|
|
179
|
+
? `Unknown connect target "${target}". Supported: claude-code, codex.\n`
|
|
180
|
+
: `Usage: ${DISTRIBUTION.commandName} connect <claude-code|codex> [--project] [--off]\n`);
|
|
181
|
+
return 1;
|
|
182
|
+
}
|
|
183
|
+
const profile = await (deps.readProfile ?? readActiveProfile)();
|
|
184
|
+
const helperCommand = `${DISTRIBUTION.commandName} token`;
|
|
185
|
+
const path = settingsPathFor(project ? "project" : "user", deps.cwd ?? process.cwd(), deps.home);
|
|
186
|
+
let existing;
|
|
187
|
+
try {
|
|
188
|
+
existing = readSettings(path);
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
stderr(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
192
|
+
return 1;
|
|
193
|
+
}
|
|
194
|
+
if (off) {
|
|
195
|
+
const plan = planDisconnect(existing, DISTRIBUTION.commandName);
|
|
196
|
+
writeSettings(path, plan.settings);
|
|
197
|
+
stdout(plan.removed.length > 0
|
|
198
|
+
? `✓ Claude Code disconnected from YAGNI.\n ${path}\n Removed: ${plan.removed.join(", ")}\n`
|
|
199
|
+
: `Nothing to disconnect in ${path}.\n`);
|
|
200
|
+
if (plan.keptForeignHelper) {
|
|
201
|
+
stdout(` Kept apiKeyHelper (${plan.keptForeignHelper}) since it is not YAGNI's.\n`);
|
|
202
|
+
}
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
const creds = credentialsFromProfile(profile);
|
|
206
|
+
if (!creds?.token) {
|
|
207
|
+
stderr(`Not logged in to environment "${profile.name}" (${profile.baseUrl}). Run \`${DISTRIBUTION.commandName} login\` first.\n`);
|
|
208
|
+
return 1;
|
|
209
|
+
}
|
|
210
|
+
const plan = planConnect(existing, { baseUrl: profile.baseUrl, helperCommand });
|
|
211
|
+
writeSettings(path, plan.settings);
|
|
212
|
+
stdout(`✓ Claude Code connected to YAGNI (${profile.name} → ${profile.baseUrl}).\n`);
|
|
213
|
+
stdout(` ${path}\n`);
|
|
214
|
+
for (const change of plan.changes)
|
|
215
|
+
stdout(` ${change}\n`);
|
|
216
|
+
for (const key of plan.replaced) {
|
|
217
|
+
stdout(` Replaced an existing ${key}. \`connect claude-code --off\` removes YAGNI's value but cannot restore the old one.\n`);
|
|
218
|
+
}
|
|
219
|
+
const shellEnv = deps.env ?? process.env;
|
|
220
|
+
for (const key of ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"]) {
|
|
221
|
+
if (shellEnv[key]) {
|
|
222
|
+
stdout(` ⚠ ${key} is set in your shell and OUTRANKS the settings written here. Unset it or Claude Code will keep using it.\n`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
stdout(` Restart Claude Code to pick this up. Model picks map to YAGNI tiers (fable→peak, opus→advanced, sonnet→standard, haiku→efficient).\n`);
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=connectClaudeCode.js.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `yagni connect codex` — point Codex CLI at the YAGNI model proxy.
|
|
3
|
+
*
|
|
4
|
+
* Writes into `~/.codex/config.toml` (respecting CODEX_HOME):
|
|
5
|
+
*
|
|
6
|
+
* model_provider = "yagni" the active provider
|
|
7
|
+
* model = "advanced" a YAGNI tier — Codex sends it
|
|
8
|
+
* verbatim and the proxy's catalog
|
|
9
|
+
* enforcement validates it
|
|
10
|
+
* [model_providers.yagni] base_url → `<base>/v1` (Codex
|
|
11
|
+
* appends /responses — the proxy's
|
|
12
|
+
* Responses-dialect route),
|
|
13
|
+
* wire_api "responses" (the only
|
|
14
|
+
* wire current Codex speaks),
|
|
15
|
+
* x-yagni-caller: codex attribution,
|
|
16
|
+
* and auth.command = `yagni token` —
|
|
17
|
+
* Codex's command-backed bearer
|
|
18
|
+
* token, so no secret is ever baked
|
|
19
|
+
* into the config and rotation rides
|
|
20
|
+
* the same refresh client as
|
|
21
|
+
* everything else.
|
|
22
|
+
*
|
|
23
|
+
* TOML cannot be comment-preservingly round-tripped by a parser, so when the
|
|
24
|
+
* existing config carries comments we save a one-time `config.toml.yagni-backup`
|
|
25
|
+
* next to it before rewriting and say so — never silently eat a user's notes.
|
|
26
|
+
* Everything else follows the claude-code connector's contract: managed keys
|
|
27
|
+
* only, atomic write, symlink refusal, corrupt file is a loud error, `--off`
|
|
28
|
+
* removes exactly what we own.
|
|
29
|
+
*/
|
|
30
|
+
import { type Profile } from "./profiles.js";
|
|
31
|
+
export declare const CODEX_PROVIDER_ID = "yagni";
|
|
32
|
+
export declare const CODEX_DEFAULT_TIER = "advanced";
|
|
33
|
+
type TomlTable = Record<string, unknown>;
|
|
34
|
+
export declare function codexConfigPath(home?: string, env?: NodeJS.ProcessEnv): string;
|
|
35
|
+
export interface CodexConnectPlan {
|
|
36
|
+
config: TomlTable;
|
|
37
|
+
changes: string[];
|
|
38
|
+
replaced: string[];
|
|
39
|
+
}
|
|
40
|
+
/** Pure: the config after connecting, plus what changed. */
|
|
41
|
+
export declare function planConnectCodex(existing: TomlTable, input: {
|
|
42
|
+
baseUrl: string;
|
|
43
|
+
commandName: string;
|
|
44
|
+
}): CodexConnectPlan;
|
|
45
|
+
export interface CodexDisconnectPlan {
|
|
46
|
+
config: TomlTable;
|
|
47
|
+
removed: string[];
|
|
48
|
+
}
|
|
49
|
+
/** Pure: the config after `--off` — our provider and its selection out. */
|
|
50
|
+
export declare function planDisconnectCodex(existing: TomlTable): CodexDisconnectPlan;
|
|
51
|
+
export interface CodexConfigFile {
|
|
52
|
+
config: TomlTable;
|
|
53
|
+
/** The raw text, kept so a comment-carrying file can be backed up. */
|
|
54
|
+
raw: string | null;
|
|
55
|
+
}
|
|
56
|
+
/** Read + parse the Codex config. Missing → {}. Corrupt/symlink → throws. */
|
|
57
|
+
export declare function readCodexConfig(path: string): CodexConfigFile;
|
|
58
|
+
/**
|
|
59
|
+
* Atomic write; when the original text carried comments (which a parse →
|
|
60
|
+
* stringify round-trip cannot preserve), a `config.toml.yagni-backup` copy of
|
|
61
|
+
* the original is written first. Returns the backup path when one was made.
|
|
62
|
+
*/
|
|
63
|
+
export declare function writeCodexConfig(path: string, config: TomlTable, originalRaw: string | null): string | null;
|
|
64
|
+
export interface ConnectCodexDeps {
|
|
65
|
+
readProfile?: () => Promise<Profile>;
|
|
66
|
+
home?: string;
|
|
67
|
+
env?: NodeJS.ProcessEnv;
|
|
68
|
+
stdout?: (text: string) => void;
|
|
69
|
+
stderr?: (text: string) => void;
|
|
70
|
+
}
|
|
71
|
+
export declare function connectCodexCommand(opts: {
|
|
72
|
+
off: boolean;
|
|
73
|
+
}, deps?: ConnectCodexDeps): Promise<number>;
|
|
74
|
+
export {};
|
|
75
|
+
//# sourceMappingURL=connectCodex.d.ts.map
|