troxy-cli 1.29.16 → 1.29.18
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/bin/troxy.js +33 -0
- package/package.json +1 -1
- package/src/init.js +139 -12
- package/src/tests/tool_detect.test.js +1 -1
package/bin/troxy.js
CHANGED
|
@@ -127,6 +127,39 @@ switch (command) {
|
|
|
127
127
|
process.exit(1);
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
// ── Token-efficiency instructions in AGENTS.md / global CLAUDE.md ──────
|
|
131
|
+
// --project scopes enable/disable/status to just this directory's
|
|
132
|
+
// AGENTS.md; default (no flag) is the global ~/.claude/CLAUDE.md, matching
|
|
133
|
+
// what the init/update prompt now recommends by default.
|
|
134
|
+
case 'concise': {
|
|
135
|
+
const {
|
|
136
|
+
maybeEnableConciseInstructions, removeConciseInstructions, conciseInstructionsConfigured,
|
|
137
|
+
enableGlobalConciseInstructions, removeGlobalConciseInstructions, globalConciseConfigured,
|
|
138
|
+
} = await import('../src/init.js');
|
|
139
|
+
const dir = process.cwd();
|
|
140
|
+
const projectOnly = Boolean(flags.project);
|
|
141
|
+
|
|
142
|
+
if (sub === 'enable') {
|
|
143
|
+
if (projectOnly) { await maybeEnableConciseInstructions('project', dir); }
|
|
144
|
+
else { console.log(`\n Added token-efficiency instructions to ${enableGlobalConciseInstructions()} (all projects)\n`); }
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
if (sub === 'disable') {
|
|
148
|
+
const removed = projectOnly ? removeConciseInstructions(dir) : removeGlobalConciseInstructions();
|
|
149
|
+
console.log(removed ? '\n Removed ✓\n' : `\n Not enabled ${projectOnly ? 'in this project' : 'globally'}.\n`);
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
if (sub === 'status' || !sub) {
|
|
153
|
+
console.log('');
|
|
154
|
+
console.log(` Global (~/.claude/CLAUDE.md): ${globalConciseConfigured() ? 'enabled' : 'not enabled'}`);
|
|
155
|
+
console.log(` This project (./AGENTS.md): ${conciseInstructionsConfigured(dir) ? 'enabled' : 'not enabled'}`);
|
|
156
|
+
console.log('');
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
console.error(` Unknown: troxy concise ${sub}. Try: status, enable, disable (add --project to scope to this directory)\n`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
|
|
130
163
|
// ── Auth ──────────────────────────────────────────────────────
|
|
131
164
|
case 'login':
|
|
132
165
|
await runLogin(flags);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "troxy-cli",
|
|
3
|
-
"version": "1.29.
|
|
3
|
+
"version": "1.29.18",
|
|
4
4
|
"description": "A secure control layer for AI agents: policies across payments, messages, logins, destructive actions, model usage, and secrets, all enforceable from the CLI",
|
|
5
5
|
"homepage": "https://troxy.io",
|
|
6
6
|
"bugs": {
|
package/src/init.js
CHANGED
|
@@ -81,7 +81,7 @@ export async function refreshHostedInstructions(key) {
|
|
|
81
81
|
return true;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
export async function reprovisionKeyConsumers(key, agentName, proxyOptIn = null) {
|
|
84
|
+
export async function reprovisionKeyConsumers(key, agentName, proxyOptIn = null, conciseOptIn = null) {
|
|
85
85
|
const platform = process.platform;
|
|
86
86
|
const { detected, hasOpenClaw, hasClaude, none } = detectMcpClients();
|
|
87
87
|
|
|
@@ -163,6 +163,13 @@ export async function reprovisionKeyConsumers(key, agentName, proxyOptIn = null)
|
|
|
163
163
|
console.log('\n Restart your MCP client to activate Troxy.');
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
// Own opt-in, outside the client-detection if/else above: AGENTS.md is a
|
|
167
|
+
// cross-tool convention, not tied to which specific MCP client got patched.
|
|
168
|
+
// Idempotent (conciseInstructionsConfigured short-circuits), so it's safe
|
|
169
|
+
// to run on every reprovision - fresh init AND `troxy update`/rotate-key -
|
|
170
|
+
// not just the first-ever connect, without re-prompting once it's set.
|
|
171
|
+
await maybeEnableConciseInstructions(conciseOptIn, process.cwd());
|
|
172
|
+
|
|
166
173
|
console.log('\n Setting up background service...');
|
|
167
174
|
const viaEphemeralNpx = ranViaEphemeralNpx();
|
|
168
175
|
try {
|
|
@@ -195,6 +202,23 @@ export async function reprovisionKeyConsumers(key, agentName, proxyOptIn = null)
|
|
|
195
202
|
const AGENTS_START = '<!-- troxy:start -->';
|
|
196
203
|
const AGENTS_END = '<!-- troxy:end -->';
|
|
197
204
|
|
|
205
|
+
// A separate marker pair, not a section inside AGENTS_START/END: the security
|
|
206
|
+
// checkpoint block above is load-bearing (Troxy doesn't function without it)
|
|
207
|
+
// and always written. Concise instructions are a style preference the user
|
|
208
|
+
// opts into separately - same file, independently addable/removable without
|
|
209
|
+
// touching or re-flowing the checkpoint block.
|
|
210
|
+
const CONCISE_START = '<!-- troxy:concise:start -->';
|
|
211
|
+
const CONCISE_END = '<!-- troxy:concise:end -->';
|
|
212
|
+
|
|
213
|
+
const CONCISE_INSTRUCTIONS = `## Style (via Troxy)
|
|
214
|
+
|
|
215
|
+
- Read existing files before writing. Don't re-read unless changed.
|
|
216
|
+
- Thorough in reasoning, concise in output.
|
|
217
|
+
- Skip files over 100KB unless required.
|
|
218
|
+
- No sycophantic openers or closing fluff.
|
|
219
|
+
- No emojis or em-dashes.
|
|
220
|
+
- Do not guess APIs, versions, flags, commit SHAs, or package names. Verify by reading code or docs before asserting.`;
|
|
221
|
+
|
|
198
222
|
// The endpoint deliberately does not echo the caller's key back, so the live
|
|
199
223
|
// key is substituted here, on the machine that already has it. Split/join
|
|
200
224
|
// rather than a regex because a key is user-controlled input and would other-
|
|
@@ -240,29 +264,122 @@ async function printHostedAgentSetup(key) {
|
|
|
240
264
|
console.log(' Then follow it before you pay, email, log in, or delete anything.\n');
|
|
241
265
|
}
|
|
242
266
|
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
267
|
+
// Claude Code's real, documented global instructions file - not the same
|
|
268
|
+
// thing as a bare ~/AGENTS.md, which (per our own testing) a project working
|
|
269
|
+
// directory does not walk up to. This is the one path that actually applies
|
|
270
|
+
// everywhere, so it's the file the "all projects" option below writes to.
|
|
271
|
+
function globalClaudeMdPath() {
|
|
272
|
+
return path.join(os.homedir(), '.claude', 'CLAUDE.md');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Same merge algorithm as writeAgentsFile (marker-delimited, never clobbers
|
|
276
|
+
// content outside its own span), generalized to any file path so both the
|
|
277
|
+
// per-project AGENTS.md and the global CLAUDE.md can share it.
|
|
278
|
+
function writeMarkedBlock(file, text, startMarker, endMarker) {
|
|
279
|
+
const block = `${startMarker}\n${text}\n${endMarker}`;
|
|
249
280
|
let existing = '';
|
|
250
281
|
try { existing = fs.readFileSync(file, 'utf8'); } catch {}
|
|
251
282
|
|
|
252
283
|
let next;
|
|
253
|
-
if (existing.includes(
|
|
254
|
-
const head = existing.slice(0, existing.indexOf(
|
|
255
|
-
const tail = existing.slice(existing.indexOf(
|
|
284
|
+
if (existing.includes(startMarker) && existing.includes(endMarker)) {
|
|
285
|
+
const head = existing.slice(0, existing.indexOf(startMarker));
|
|
286
|
+
const tail = existing.slice(existing.indexOf(endMarker) + endMarker.length);
|
|
256
287
|
next = head + block + tail;
|
|
257
288
|
} else if (existing.trim()) {
|
|
258
289
|
next = existing.replace(/\s*$/, '') + '\n\n' + block + '\n';
|
|
259
290
|
} else {
|
|
260
291
|
next = block + '\n';
|
|
261
292
|
}
|
|
293
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
262
294
|
fs.writeFileSync(file, next);
|
|
263
295
|
return file;
|
|
264
296
|
}
|
|
265
297
|
|
|
298
|
+
function removeMarkedBlock(file, startMarker, endMarker) {
|
|
299
|
+
let existing;
|
|
300
|
+
try { existing = fs.readFileSync(file, 'utf8'); } catch { return false; }
|
|
301
|
+
if (!existing.includes(startMarker) || !existing.includes(endMarker)) return false;
|
|
302
|
+
const head = existing.slice(0, existing.indexOf(startMarker));
|
|
303
|
+
const tail = existing.slice(existing.indexOf(endMarker) + endMarker.length);
|
|
304
|
+
fs.writeFileSync(file, (head + tail).replace(/\n{3,}/g, '\n\n'));
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function hasMarkedBlock(file, startMarker, endMarker) {
|
|
309
|
+
try {
|
|
310
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
311
|
+
return text.includes(startMarker) && text.includes(endMarker);
|
|
312
|
+
} catch { return false; }
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Idempotent, and never clobbers instructions the user wrote themselves: the
|
|
316
|
+
// Troxy section lives between markers, so re-running init (or rotate-key,
|
|
317
|
+
// which changes the key inside the block) replaces only that span. Each
|
|
318
|
+
// marker pair is independent - writing/removing one never touches another's
|
|
319
|
+
// span or anything the user wrote outside all of them.
|
|
320
|
+
export function writeAgentsFile(dir, text, startMarker = AGENTS_START, endMarker = AGENTS_END) {
|
|
321
|
+
return writeMarkedBlock(path.join(dir, 'AGENTS.md'), text, startMarker, endMarker);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function conciseInstructionsConfigured(dir) {
|
|
325
|
+
return hasMarkedBlock(path.join(dir, 'AGENTS.md'), CONCISE_START, CONCISE_END);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function removeConciseInstructions(dir) {
|
|
329
|
+
return removeMarkedBlock(path.join(dir, 'AGENTS.md'), CONCISE_START, CONCISE_END);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function globalConciseConfigured() {
|
|
333
|
+
return hasMarkedBlock(globalClaudeMdPath(), CONCISE_START, CONCISE_END);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function removeGlobalConciseInstructions() {
|
|
337
|
+
return removeMarkedBlock(globalClaudeMdPath(), CONCISE_START, CONCISE_END);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function enableGlobalConciseInstructions() {
|
|
341
|
+
return writeMarkedBlock(globalClaudeMdPath(), CONCISE_INSTRUCTIONS, CONCISE_START, CONCISE_END);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// conciseOptIn is one of: 'global', 'project', false (explicitly skip), or
|
|
345
|
+
// null (ask interactively). Global first, and recommended by default in the
|
|
346
|
+
// prompt: it's the one location Claude Code always reads regardless of which
|
|
347
|
+
// directory you're in, so it structurally can't land in the wrong place the
|
|
348
|
+
// way the project-scoped option did on its first real run. Never claims a
|
|
349
|
+
// specific percentage in the copy below - the one number we measured for
|
|
350
|
+
// this came from a flawed test (subagents don't reliably load these files
|
|
351
|
+
// the way a real session does) and shouldn't be repeated as fact.
|
|
352
|
+
export async function maybeEnableConciseInstructions(conciseOptIn, dir) {
|
|
353
|
+
if (globalConciseConfigured() || conciseInstructionsConfigured(dir)) return;
|
|
354
|
+
|
|
355
|
+
let choice;
|
|
356
|
+
if (conciseOptIn === 'global' || conciseOptIn === 'project' || conciseOptIn === false) {
|
|
357
|
+
choice = conciseOptIn;
|
|
358
|
+
} else if (!process.stdin.isTTY) {
|
|
359
|
+
choice = false;
|
|
360
|
+
} else {
|
|
361
|
+
console.log('\n Add token-efficiency instructions to make Claude Code\'s responses');
|
|
362
|
+
console.log(' more concise? Trade-off: less verbose can mean less detail.');
|
|
363
|
+
console.log('');
|
|
364
|
+
console.log(' [G] All projects (recommended) - writes once to ~/.claude/CLAUDE.md');
|
|
365
|
+
console.log(' [P] This project only - writes to ./AGENTS.md');
|
|
366
|
+
console.log(' [N] Skip');
|
|
367
|
+
console.log('');
|
|
368
|
+
console.log(' Remove any time with: troxy concise disable (add --project for just this one)');
|
|
369
|
+
const ans = (await prompt(' Choice (G/p/n): ')).toLowerCase();
|
|
370
|
+
choice = ans === 'p' ? 'project' : ans === 'n' ? false : 'global';
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (choice === false) return;
|
|
374
|
+
if (choice === 'global') {
|
|
375
|
+
const file = enableGlobalConciseInstructions();
|
|
376
|
+
console.log(` Added token-efficiency instructions to ${file} (all projects)`);
|
|
377
|
+
} else {
|
|
378
|
+
const file = writeAgentsFile(dir, CONCISE_INSTRUCTIONS, CONCISE_START, CONCISE_END);
|
|
379
|
+
console.log(` Added token-efficiency instructions to ${file} (this project)`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
266
383
|
export function prompt(question) {
|
|
267
384
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
268
385
|
return new Promise(resolve => rl.question(question, ans => { rl.close(); resolve(ans.trim()); }));
|
|
@@ -317,10 +434,20 @@ const MCP_CLIENTS = [
|
|
|
317
434
|
},
|
|
318
435
|
];
|
|
319
436
|
|
|
320
|
-
export async function runInit({ key, name, proxy } = {}) {
|
|
437
|
+
export async function runInit({ key, name, proxy, concise } = {}) {
|
|
321
438
|
// undefined (flag never passed) -> null, so reprovisionKeyConsumers can
|
|
322
439
|
// tell "not decided, ask interactively" apart from an explicit --proxy.
|
|
323
440
|
const proxyOptIn = proxy === undefined ? null : Boolean(proxy);
|
|
441
|
+
// Same null-means-ask-interactively shape as proxyOptIn, but three-valued:
|
|
442
|
+
// --concise alone (bare flag, parses as boolean true) means the
|
|
443
|
+
// recommended default, 'global'; --concise project or --concise global
|
|
444
|
+
// pick explicitly; --concise false skips. Omit the flag entirely for the
|
|
445
|
+
// default (interactive ask, or off when non-interactive).
|
|
446
|
+
const conciseOptIn =
|
|
447
|
+
concise === undefined ? null :
|
|
448
|
+
concise === true ? 'global' :
|
|
449
|
+
concise === 'false' ? false :
|
|
450
|
+
concise;
|
|
324
451
|
if (!key || !key.startsWith('txy-')) {
|
|
325
452
|
console.error('\n Error: --key is required and must start with txy-');
|
|
326
453
|
console.error(' Usage: npx troxy-cli init --key txy-...\n');
|
|
@@ -381,7 +508,7 @@ export async function runInit({ key, name, proxy } = {}) {
|
|
|
381
508
|
// Non-fatal: the daemon's own heartbeat will retry this later.
|
|
382
509
|
}
|
|
383
510
|
|
|
384
|
-
await reprovisionKeyConsumers(key, agentName, proxyOptIn);
|
|
511
|
+
await reprovisionKeyConsumers(key, agentName, proxyOptIn, conciseOptIn);
|
|
385
512
|
|
|
386
513
|
// Scan for local AI coding tools so the dashboard's "My AI tools" view has
|
|
387
514
|
// something to show. Non-fatal - a scan failure never breaks setup.
|
|
@@ -269,7 +269,7 @@ describe('wiring: api.js / bin/troxy.js / init.js', () => {
|
|
|
269
269
|
});
|
|
270
270
|
|
|
271
271
|
it('runInit calls tool detection after reprovisionKeyConsumers, wrapped so a scan failure never blocks init', () => {
|
|
272
|
-
const reprovisionIdx = initSrc.indexOf('reprovisionKeyConsumers(key, agentName, proxyOptIn)');
|
|
272
|
+
const reprovisionIdx = initSrc.indexOf('reprovisionKeyConsumers(key, agentName, proxyOptIn, conciseOptIn)');
|
|
273
273
|
const toolIdx = initSrc.indexOf('runToolDetection');
|
|
274
274
|
assert.ok(reprovisionIdx !== -1, 'reprovisionKeyConsumers call not found in init.js');
|
|
275
275
|
assert.ok(toolIdx !== -1, 'runToolDetection not wired into runInit');
|