troxy-cli 1.25.1 → 1.26.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 +1 -1
- package/bin/troxy.js +4 -5
- package/package.json +1 -1
- package/src/api.js +3 -2
- package/src/init.js +3 -5
- package/src/tests/tool_detect.test.js +53 -157
- package/src/tool_detect.js +20 -99
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ npx troxy-cli <command>
|
|
|
50
50
|
|---------|-------------|
|
|
51
51
|
| `troxy init` | Connect an agent to Troxy — validates API key, sets agent name, patches MCP configs |
|
|
52
52
|
| `troxy login` | Start a 12-hour CLI session (opens browser → copy code → paste into terminal) |
|
|
53
|
-
| `troxy tools` | Scan for installed AI coding tools and
|
|
53
|
+
| `troxy tools` | Scan for installed AI coding tools and report them to the dashboard |
|
|
54
54
|
| `troxy mcps` | List connected MCP agents and their status |
|
|
55
55
|
| `troxy policies` | List every org/space policy that applies to you (creating one is admin-only) |
|
|
56
56
|
| `troxy activity` | View recent transaction audit log |
|
package/bin/troxy.js
CHANGED
|
@@ -91,9 +91,8 @@ switch (command) {
|
|
|
91
91
|
troxy tools
|
|
92
92
|
|
|
93
93
|
Re-scans this machine for AI coding tools (Claude Code, Cursor, GitHub
|
|
94
|
-
Copilot, Windsurf, Continue, Aider) and
|
|
95
|
-
|
|
96
|
-
token estimates. Run this any time your plan changes.
|
|
94
|
+
Copilot, Windsurf, Continue, Aider) and reports which ones it found to the
|
|
95
|
+
dashboard's "My AI tools" view. Run this any time you install or remove one.
|
|
97
96
|
`);
|
|
98
97
|
process.exit(0);
|
|
99
98
|
}
|
|
@@ -107,7 +106,7 @@ switch (command) {
|
|
|
107
106
|
console.log('\n No supported AI coding tools detected on this machine.\n');
|
|
108
107
|
break;
|
|
109
108
|
}
|
|
110
|
-
await runToolDetection(apiKey
|
|
109
|
+
await runToolDetection(apiKey);
|
|
111
110
|
console.log(' Saved ✓ See it at https://dash.troxy.io\n');
|
|
112
111
|
break;
|
|
113
112
|
}
|
|
@@ -649,7 +648,7 @@ switch (command) {
|
|
|
649
648
|
troxy init --key <api-key> Connect this machine as an MCP + save key
|
|
650
649
|
troxy init --key <api-key> --name "My Agent" Same, no interactive prompt
|
|
651
650
|
(for cloud/scripted agents that can't answer stdin)
|
|
652
|
-
troxy tools Scan for AI tools +
|
|
651
|
+
troxy tools Scan for AI tools + report them to the dashboard
|
|
653
652
|
troxy restart Restart the MCP background service
|
|
654
653
|
troxy uninstall Remove Troxy from this machine
|
|
655
654
|
troxy status API health + MCP status (no login needed)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "troxy-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.26.0",
|
|
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/api.js
CHANGED
|
@@ -132,8 +132,9 @@ export const api = {
|
|
|
132
132
|
// MCP status (agent API key — no login needed)
|
|
133
133
|
mcpStatus: (apiKey) => request('GET', '/mcp/status', { apiKey }),
|
|
134
134
|
|
|
135
|
-
// AI tool inventory
|
|
136
|
-
// `troxy tools`. Agent API key auth, same as the
|
|
135
|
+
// AI tool inventory (presence only - no plan/price), self-reported by
|
|
136
|
+
// `troxy init` / `troxy tools`. Agent API key auth, same as the
|
|
137
|
+
// evaluate/heartbeat paths.
|
|
137
138
|
reportToolPlans: (apiKey, tools) => request('POST', '/agents/tool-plans', { apiKey, body: { tools } }),
|
|
138
139
|
|
|
139
140
|
// Setup instructions for an agent with no MCP client to configure. Fetched
|
package/src/init.js
CHANGED
|
@@ -306,13 +306,11 @@ export async function runInit({ key, name, proxy } = {}) {
|
|
|
306
306
|
|
|
307
307
|
await reprovisionKeyConsumers(key, agentName, proxyOptIn);
|
|
308
308
|
|
|
309
|
-
// Scan for local AI coding tools
|
|
310
|
-
//
|
|
311
|
-
// Non-fatal + TTY-gated inside runToolDetection - a scripted init never
|
|
312
|
-
// hangs and a scan failure never breaks setup.
|
|
309
|
+
// Scan for local AI coding tools so the dashboard's "My AI tools" view has
|
|
310
|
+
// something to show. Non-fatal - a scan failure never breaks setup.
|
|
313
311
|
try {
|
|
314
312
|
const { runToolDetection } = await import('./tool_detect.js');
|
|
315
|
-
await runToolDetection(key
|
|
313
|
+
await runToolDetection(key);
|
|
316
314
|
} catch {
|
|
317
315
|
// Tool detection is a bonus, never a blocker for a successful init.
|
|
318
316
|
}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
// tool_detect.js: the AI-tool
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
1
|
+
// tool_detect.js: the AI-tool presence scan wired into `troxy init` (after
|
|
2
|
+
// reprovisionKeyConsumers) and standalone `troxy tools`. A single machine
|
|
3
|
+
// can have more than one of these six tools installed at once, and each is
|
|
4
|
+
// tracked independently - never assume "one AI tool per user" (that's the
|
|
5
|
+
// whole reason detectAiTools() returns an array, not a single match).
|
|
6
|
+
//
|
|
7
|
+
// The self-reported subscription-plan concept this file used to test
|
|
8
|
+
// (promptForToolPlans, silentToolReport, a per-tool `plans` menu) has been
|
|
9
|
+
// removed entirely - a member's own guess should never feed cost math again
|
|
10
|
+
// (see troxy-tf-live's dashboard.py handle_token_analysis). What's left is
|
|
11
|
+
// presence-only: detect, then report {slug, installed:true} with nothing
|
|
12
|
+
// else attached.
|
|
7
13
|
//
|
|
8
14
|
// Path-based detection (cursor/github_copilot/windsurf/continue) runs
|
|
9
15
|
// against a real temp HOME, the same technique config.test.js and
|
|
@@ -37,8 +43,6 @@ process.env.LOCALAPPDATA = path.join(TMP, 'AppData/Local');
|
|
|
37
43
|
|
|
38
44
|
const {
|
|
39
45
|
detectAiTools,
|
|
40
|
-
promptForToolPlans,
|
|
41
|
-
silentToolReport,
|
|
42
46
|
reportToolPlans,
|
|
43
47
|
runToolDetection,
|
|
44
48
|
binExists,
|
|
@@ -59,25 +63,11 @@ describe('TOOLS registry', () => {
|
|
|
59
63
|
);
|
|
60
64
|
});
|
|
61
65
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
cursor: ['hobby', 'pro', 'business', 'not_sure'],
|
|
68
|
-
github_copilot: ['individual', 'business', 'enterprise', 'not_sure'],
|
|
69
|
-
windsurf: ['free', 'pro', 'not_sure'],
|
|
70
|
-
continue: ['free', 'pro', 'not_sure'],
|
|
71
|
-
aider: ['free', 'pro', 'not_sure'],
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
for (const [slug, expected] of Object.entries(EXPECTED_PLAN_SLUGS)) {
|
|
75
|
-
it(`${slug} offers exactly the plan slugs the backend registry expects`, () => {
|
|
76
|
-
const tool = TOOLS.find(t => t.slug === slug);
|
|
77
|
-
assert.ok(tool, `${slug} missing from TOOLS`);
|
|
78
|
-
assert.deepEqual(tool.plans.map(p => p.slug), expected);
|
|
79
|
-
});
|
|
80
|
-
}
|
|
66
|
+
it('carries no plan/price data any more - presence only', () => {
|
|
67
|
+
for (const t of TOOLS) {
|
|
68
|
+
assert.equal(t.plans, undefined, `${t.slug} must not have a plans array`);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
81
71
|
});
|
|
82
72
|
|
|
83
73
|
describe('binExists (the mechanism behind claude_code/aider detection)', () => {
|
|
@@ -97,7 +87,7 @@ describe('binExists (the mechanism behind claude_code/aider detection)', () => {
|
|
|
97
87
|
describe('claude_code / aider detection wiring', () => {
|
|
98
88
|
it('claude_code reuses init.js\'s hasClaudeCode() rather than reimplementing the probe', () => {
|
|
99
89
|
assert.ok(
|
|
100
|
-
toolDetectSrc.includes("import {
|
|
90
|
+
toolDetectSrc.includes("import { hasClaudeCode } from './init.js'"),
|
|
101
91
|
'tool_detect.js must import hasClaudeCode from init.js, not redeclare its own execSync probe',
|
|
102
92
|
);
|
|
103
93
|
const claudeBlock = toolDetectSrc.slice(
|
|
@@ -178,28 +168,47 @@ describe('detectAiTools - path-based markers (cursor / github_copilot / windsurf
|
|
|
178
168
|
});
|
|
179
169
|
});
|
|
180
170
|
|
|
181
|
-
describe('
|
|
182
|
-
|
|
171
|
+
describe('reportToolPlans (presence-only report)', () => {
|
|
172
|
+
let originalReportToolPlans;
|
|
173
|
+
beforeEach(() => { originalReportToolPlans = api.reportToolPlans; });
|
|
174
|
+
afterEach(() => { api.reportToolPlans = originalReportToolPlans; });
|
|
175
|
+
|
|
176
|
+
it('POSTs only installed tools, as bare {slug, installed:true} - no plan, no cycle_day', async () => {
|
|
177
|
+
let captured;
|
|
178
|
+
api.reportToolPlans = async (apiKey, tools) => { captured = { apiKey, tools }; return {}; };
|
|
183
179
|
const detected = [
|
|
184
180
|
{ slug: 'claude_code', name: 'Claude Code', installed: true },
|
|
185
181
|
{ slug: 'cursor', name: 'Cursor', installed: false },
|
|
186
182
|
{ slug: 'aider', name: 'Aider', installed: true },
|
|
187
183
|
];
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
184
|
+
await reportToolPlans('txy-key', detected);
|
|
185
|
+
assert.deepEqual(captured, {
|
|
186
|
+
apiKey: 'txy-key',
|
|
187
|
+
tools: [
|
|
188
|
+
{ slug: 'claude_code', installed: true },
|
|
189
|
+
{ slug: 'aider', installed: true },
|
|
190
|
+
],
|
|
191
|
+
});
|
|
192
192
|
});
|
|
193
193
|
|
|
194
|
-
it('
|
|
195
|
-
|
|
194
|
+
it('makes no API call when nothing is installed', async () => {
|
|
195
|
+
let called = false;
|
|
196
|
+
api.reportToolPlans = async () => { called = true; };
|
|
197
|
+
await reportToolPlans('txy-key', [{ slug: 'cursor', name: 'Cursor', installed: false }]);
|
|
198
|
+
assert.equal(called, false);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('swallows a failed report rather than throwing - must never break `troxy init`', async () => {
|
|
202
|
+
api.reportToolPlans = async () => { throw new Error('network down'); };
|
|
203
|
+
await assert.doesNotReject(
|
|
204
|
+
reportToolPlans('txy-key', [{ slug: 'cursor', name: 'Cursor', installed: true }]),
|
|
205
|
+
);
|
|
196
206
|
});
|
|
197
207
|
});
|
|
198
208
|
|
|
199
|
-
describe('runToolDetection
|
|
209
|
+
describe('runToolDetection', () => {
|
|
200
210
|
let calls;
|
|
201
211
|
let originalReportToolPlans;
|
|
202
|
-
let originalIsTTY;
|
|
203
212
|
|
|
204
213
|
before(() => {
|
|
205
214
|
// A deterministic installed tool, independent of whether claude/aider
|
|
@@ -214,16 +223,13 @@ describe('runToolDetection - non-interactive guard (a scripted/CI init must neve
|
|
|
214
223
|
calls = [];
|
|
215
224
|
originalReportToolPlans = api.reportToolPlans;
|
|
216
225
|
api.reportToolPlans = async (apiKey, tools) => { calls.push({ apiKey, tools }); return {}; };
|
|
217
|
-
originalIsTTY = process.stdin.isTTY;
|
|
218
226
|
});
|
|
219
227
|
afterEach(() => {
|
|
220
228
|
api.reportToolPlans = originalReportToolPlans;
|
|
221
|
-
process.stdin.isTTY = originalIsTTY;
|
|
222
229
|
});
|
|
223
230
|
|
|
224
|
-
it('
|
|
225
|
-
|
|
226
|
-
await runToolDetection('txy-test-key', { interactive: true });
|
|
231
|
+
it('reports every detected tool as bare presence - no prompt, no stdin involved', async () => {
|
|
232
|
+
await runToolDetection('txy-test-key');
|
|
227
233
|
|
|
228
234
|
assert.equal(calls.length, 1, 'reportToolPlans must be called exactly once when at least one tool is installed');
|
|
229
235
|
assert.equal(calls[0].apiKey, 'txy-test-key');
|
|
@@ -231,125 +237,15 @@ describe('runToolDetection - non-interactive guard (a scripted/CI init must neve
|
|
|
231
237
|
const cont = tools.find(t => t.slug === 'continue');
|
|
232
238
|
assert.ok(cont, 'the tool detected via the temp-HOME marker must be in the reported set');
|
|
233
239
|
for (const t of tools) {
|
|
240
|
+
assert.deepEqual(Object.keys(t).sort(), ['installed', 'slug']);
|
|
234
241
|
assert.equal(t.installed, true);
|
|
235
|
-
assert.equal(t.plan, 'not_sure');
|
|
236
|
-
assert.equal(t.cycle_day, null);
|
|
237
242
|
}
|
|
238
243
|
});
|
|
239
244
|
|
|
240
|
-
it('returns promptly
|
|
241
|
-
process.stdin.isTTY = false;
|
|
245
|
+
it('returns promptly - nothing here ever waits on stdin', async () => {
|
|
242
246
|
const start = Date.now();
|
|
243
|
-
await runToolDetection('txy-test-key'
|
|
244
|
-
assert.ok(Date.now() - start < 2000, 'runToolDetection must
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it('runToolDetection only takes the interactive path when both interactive AND stdin.isTTY are true', () => {
|
|
248
|
-
assert.ok(
|
|
249
|
-
toolDetectSrc.includes('(interactive && process.stdin.isTTY)'),
|
|
250
|
-
'the non-interactive guard must check both interactive and process.stdin.isTTY, not just one',
|
|
251
|
-
);
|
|
252
|
-
});
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
describe("promptForToolPlans - returns immediately when nothing is installed (no prompt call, no stdin needed)", () => {
|
|
256
|
-
it('resolves to [] without touching stdin', async () => {
|
|
257
|
-
const result = await promptForToolPlans([{ slug: 'cursor', name: 'Cursor', installed: false }]);
|
|
258
|
-
assert.deepEqual(result, []);
|
|
259
|
-
});
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
describe('promptForToolPlans numbered-choice parsing (mirrors the real logic in tool_detect.js)', () => {
|
|
263
|
-
// Mirrors the parsing inside promptForToolPlans's loop: a numbered-menu
|
|
264
|
-
// answer maps 1-based to the plan array; anything out of range - blank,
|
|
265
|
-
// 0, a too-large number, non-numeric junk - degrades to 'not_sure' rather
|
|
266
|
-
// than re-prompting, so a stray keystroke can never hang setup. The last
|
|
267
|
-
// test in each block below source-checks that the real implementation
|
|
268
|
-
// still contains this exact logic, so the two can't silently drift apart.
|
|
269
|
-
function resolvePlanChoice(ans, plans) {
|
|
270
|
-
const idx = parseInt(ans, 10) - 1;
|
|
271
|
-
return (idx >= 0 && idx < plans.length) ? plans[idx].slug : 'not_sure';
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const cursorPlans = TOOLS.find(t => t.slug === 'cursor').plans; // hobby, pro, business, not_sure
|
|
275
|
-
|
|
276
|
-
it('maps a valid 1-based numbered choice to the matching plan slug', () => {
|
|
277
|
-
assert.equal(resolvePlanChoice('1', cursorPlans), 'hobby');
|
|
278
|
-
assert.equal(resolvePlanChoice('2', cursorPlans), 'pro');
|
|
279
|
-
assert.equal(resolvePlanChoice('3', cursorPlans), 'business');
|
|
280
|
-
assert.equal(resolvePlanChoice('4', cursorPlans), 'not_sure');
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
it('defaults to not_sure on a blank answer (Enter to skip)', () => {
|
|
284
|
-
assert.equal(resolvePlanChoice('', cursorPlans), 'not_sure');
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
it('defaults to not_sure on an out-of-range or non-numeric answer, rather than throwing', () => {
|
|
288
|
-
assert.equal(resolvePlanChoice('99', cursorPlans), 'not_sure');
|
|
289
|
-
assert.equal(resolvePlanChoice('0', cursorPlans), 'not_sure');
|
|
290
|
-
assert.equal(resolvePlanChoice('-1', cursorPlans), 'not_sure');
|
|
291
|
-
assert.equal(resolvePlanChoice('banana', cursorPlans), 'not_sure');
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
it('the real implementation uses this exact idx = parseInt(ans, 10) - 1 mapping', () => {
|
|
295
|
-
assert.ok(toolDetectSrc.includes('const idx = parseInt(ans, 10) - 1;'));
|
|
296
|
-
assert.ok(toolDetectSrc.includes("(idx >= 0 && idx < tool.plans.length) ? tool.plans[idx].slug : 'not_sure'"));
|
|
297
|
-
});
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
describe('promptForToolPlans cycle_day parsing (mirrors the real logic)', () => {
|
|
301
|
-
function resolveCycleDay(ans) {
|
|
302
|
-
const day = parseInt(ans, 10);
|
|
303
|
-
return (day >= 1 && day <= 31) ? day : null;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
it('accepts a valid day 1-31', () => {
|
|
307
|
-
assert.equal(resolveCycleDay('1'), 1);
|
|
308
|
-
assert.equal(resolveCycleDay('15'), 15);
|
|
309
|
-
assert.equal(resolveCycleDay('31'), 31);
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it('rejects out-of-range or non-numeric input as null (Enter to skip), never throws', () => {
|
|
313
|
-
assert.equal(resolveCycleDay(''), null);
|
|
314
|
-
assert.equal(resolveCycleDay('0'), null);
|
|
315
|
-
assert.equal(resolveCycleDay('32'), null);
|
|
316
|
-
assert.equal(resolveCycleDay('abc'), null);
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
it('the real implementation uses this exact 1-31 range check', () => {
|
|
320
|
-
assert.ok(toolDetectSrc.includes('if (day >= 1 && day <= 31) cycle_day = day;'));
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
it('only asks for a renewal day on a real paid plan - never free/hobby/not_sure/api', () => {
|
|
324
|
-
assert.ok(toolDetectSrc.includes("!['free', 'hobby', 'not_sure', 'api'].includes(plan)"));
|
|
325
|
-
});
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
describe('reportToolPlans', () => {
|
|
329
|
-
let originalReportToolPlans;
|
|
330
|
-
beforeEach(() => { originalReportToolPlans = api.reportToolPlans; });
|
|
331
|
-
afterEach(() => { api.reportToolPlans = originalReportToolPlans; });
|
|
332
|
-
|
|
333
|
-
it('POSTs the tools array via api.reportToolPlans, unchanged', async () => {
|
|
334
|
-
let captured;
|
|
335
|
-
api.reportToolPlans = async (apiKey, tools) => { captured = { apiKey, tools }; return {}; };
|
|
336
|
-
const results = [{ slug: 'cursor', installed: true, plan: 'pro', cycle_day: 5 }];
|
|
337
|
-
await reportToolPlans('txy-key', results);
|
|
338
|
-
assert.deepEqual(captured, { apiKey: 'txy-key', tools: results });
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
it('makes no API call for an empty result set', async () => {
|
|
342
|
-
let called = false;
|
|
343
|
-
api.reportToolPlans = async () => { called = true; };
|
|
344
|
-
await reportToolPlans('txy-key', []);
|
|
345
|
-
assert.equal(called, false);
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
it('swallows a failed report rather than throwing - must never break `troxy init`', async () => {
|
|
349
|
-
api.reportToolPlans = async () => { throw new Error('network down'); };
|
|
350
|
-
await assert.doesNotReject(
|
|
351
|
-
reportToolPlans('txy-key', [{ slug: 'cursor', installed: true, plan: 'pro', cycle_day: 5 }]),
|
|
352
|
-
);
|
|
247
|
+
await runToolDetection('txy-test-key');
|
|
248
|
+
assert.ok(Date.now() - start < 2000, 'runToolDetection must never block waiting on input');
|
|
353
249
|
});
|
|
354
250
|
});
|
|
355
251
|
|
package/src/tool_detect.js
CHANGED
|
@@ -3,13 +3,14 @@ import os from 'os';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { execSync } from 'child_process';
|
|
5
5
|
import { api } from './api.js';
|
|
6
|
-
import {
|
|
6
|
+
import { hasClaudeCode } from './init.js';
|
|
7
7
|
|
|
8
|
-
// AI coding tools we detect
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
8
|
+
// AI coding tools we detect on this machine. Used to mirror the backend
|
|
9
|
+
// registry (troxy-tf-live tool_plans.py TOOL_PLANS) with a `plans` list per
|
|
10
|
+
// tool - that self-reported subscription-plan concept has been removed from
|
|
11
|
+
// cost math entirely (a member's own guess should never feed a dollar total
|
|
12
|
+
// again), so this is presence-only now: does the tool look installed, yes
|
|
13
|
+
// or no.
|
|
13
14
|
//
|
|
14
15
|
// `detect()` returns true if the tool looks present. Two techniques, same as
|
|
15
16
|
// init.js's detectMcpClients: a `--version` probe for CLI tools, an
|
|
@@ -38,14 +39,6 @@ export const TOOLS = [
|
|
|
38
39
|
// --version')) rather than reimplementing it - one source of truth for
|
|
39
40
|
// "is Claude Code on PATH".
|
|
40
41
|
detect: () => hasClaudeCode(),
|
|
41
|
-
plans: [
|
|
42
|
-
{ slug: 'free', label: 'Free' },
|
|
43
|
-
{ slug: 'pro', label: 'Pro ($20/mo)' },
|
|
44
|
-
{ slug: 'max_5x', label: 'Max 5x ($100/mo)' },
|
|
45
|
-
{ slug: 'max_20x', label: 'Max 20x ($200/mo)' },
|
|
46
|
-
{ slug: 'api', label: 'API / pay-per-token' },
|
|
47
|
-
{ slug: 'not_sure', label: 'Not sure' },
|
|
48
|
-
],
|
|
49
42
|
},
|
|
50
43
|
{
|
|
51
44
|
slug: 'cursor',
|
|
@@ -55,12 +48,6 @@ export const TOOLS = [
|
|
|
55
48
|
path.join(process.env.APPDATA || home, 'Cursor'),
|
|
56
49
|
path.join(home, '.config/Cursor'),
|
|
57
50
|
]),
|
|
58
|
-
plans: [
|
|
59
|
-
{ slug: 'hobby', label: 'Hobby (free)' },
|
|
60
|
-
{ slug: 'pro', label: 'Pro ($20/mo)' },
|
|
61
|
-
{ slug: 'business', label: 'Business ($40/mo)' },
|
|
62
|
-
{ slug: 'not_sure', label: 'Not sure' },
|
|
63
|
-
],
|
|
64
51
|
},
|
|
65
52
|
{
|
|
66
53
|
slug: 'github_copilot',
|
|
@@ -70,42 +57,21 @@ export const TOOLS = [
|
|
|
70
57
|
path.join(home, '.config/github-copilot/apps.json'),
|
|
71
58
|
path.join(process.env.LOCALAPPDATA || path.join(home, 'AppData/Local'), 'github-copilot/hosts.json'),
|
|
72
59
|
]),
|
|
73
|
-
plans: [
|
|
74
|
-
{ slug: 'individual', label: 'Individual ($10/mo)' },
|
|
75
|
-
{ slug: 'business', label: 'Business ($19/mo)' },
|
|
76
|
-
{ slug: 'enterprise', label: 'Enterprise ($39/mo)' },
|
|
77
|
-
{ slug: 'not_sure', label: 'Not sure' },
|
|
78
|
-
],
|
|
79
60
|
},
|
|
80
61
|
{
|
|
81
62
|
slug: 'windsurf',
|
|
82
63
|
name: 'Windsurf',
|
|
83
64
|
detect: () => anyPathExists([path.join(home, '.codeium/windsurf')]),
|
|
84
|
-
plans: [
|
|
85
|
-
{ slug: 'free', label: 'Free' },
|
|
86
|
-
{ slug: 'pro', label: 'Pro ($15/mo)' },
|
|
87
|
-
{ slug: 'not_sure', label: 'Not sure' },
|
|
88
|
-
],
|
|
89
65
|
},
|
|
90
66
|
{
|
|
91
67
|
slug: 'continue',
|
|
92
68
|
name: 'Continue',
|
|
93
69
|
detect: () => anyPathExists([path.join(home, '.continue')]),
|
|
94
|
-
plans: [
|
|
95
|
-
{ slug: 'free', label: 'Free' },
|
|
96
|
-
{ slug: 'pro', label: 'Pro ($15/mo)' },
|
|
97
|
-
{ slug: 'not_sure', label: 'Not sure' },
|
|
98
|
-
],
|
|
99
70
|
},
|
|
100
71
|
{
|
|
101
72
|
slug: 'aider',
|
|
102
73
|
name: 'Aider',
|
|
103
74
|
detect: () => binExists('aider --version'),
|
|
104
|
-
plans: [
|
|
105
|
-
{ slug: 'free', label: 'Free (bring your own API key)' },
|
|
106
|
-
{ slug: 'pro', label: 'Pro ($15/mo)' },
|
|
107
|
-
{ slug: 'not_sure', label: 'Not sure' },
|
|
108
|
-
],
|
|
109
75
|
},
|
|
110
76
|
];
|
|
111
77
|
|
|
@@ -113,69 +79,24 @@ export function detectAiTools() {
|
|
|
113
79
|
return TOOLS.map(t => ({ slug: t.slug, name: t.name, installed: !!t.detect() }));
|
|
114
80
|
}
|
|
115
81
|
|
|
116
|
-
|
|
117
|
-
// the renewal day. Returns [{slug, installed:true, plan, cycle_day}]. A tool
|
|
118
|
-
// detected but skipped (blank / invalid choice) is reported as 'not_sure' so
|
|
119
|
-
// the dashboard still shows it's installed. Never throws on bad input - a
|
|
120
|
-
// stray keystroke degrades to 'not_sure', it doesn't abort setup.
|
|
121
|
-
export async function promptForToolPlans(detected) {
|
|
82
|
+
export async function reportToolPlans(apiKey, detected) {
|
|
122
83
|
const installed = detected.filter(d => d.installed);
|
|
123
|
-
if (installed.length === 0) return
|
|
124
|
-
|
|
125
|
-
console.log('\n Found these AI tools on this machine. Set your plan for each so');
|
|
126
|
-
console.log(' the dashboard shows what you actually pay, not raw token estimates.\n');
|
|
127
|
-
|
|
128
|
-
const results = [];
|
|
129
|
-
for (const d of installed) {
|
|
130
|
-
const tool = TOOLS.find(t => t.slug === d.slug);
|
|
131
|
-
console.log(` ${tool.name}:`);
|
|
132
|
-
tool.plans.forEach((p, i) => console.log(` ${i + 1}) ${p.label}`));
|
|
133
|
-
const ans = await prompt(` Which plan? [1-${tool.plans.length}, Enter to skip]: `);
|
|
134
|
-
const idx = parseInt(ans, 10) - 1;
|
|
135
|
-
const plan = (idx >= 0 && idx < tool.plans.length) ? tool.plans[idx].slug : 'not_sure';
|
|
136
|
-
|
|
137
|
-
let cycle_day = null;
|
|
138
|
-
const chosen = tool.plans.find(p => p.slug === plan);
|
|
139
|
-
// Only ask the renewal day for a real paid plan - "Free"/"Not sure"/API
|
|
140
|
-
// have no monthly renewal to track a usage window against.
|
|
141
|
-
const isPaid = chosen && !['free', 'hobby', 'not_sure', 'api'].includes(plan);
|
|
142
|
-
if (isPaid) {
|
|
143
|
-
const dayAns = await prompt(' What day of the month does it renew? [1-31, Enter to skip]: ');
|
|
144
|
-
const day = parseInt(dayAns, 10);
|
|
145
|
-
if (day >= 1 && day <= 31) cycle_day = day;
|
|
146
|
-
}
|
|
147
|
-
results.push({ slug: d.slug, installed: true, plan, cycle_day });
|
|
148
|
-
console.log('');
|
|
149
|
-
}
|
|
150
|
-
return results;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Non-interactive fallback (piped stdin / CI): report what's installed as
|
|
154
|
-
// 'not_sure' with no prompt, so a scripted `troxy init` never hangs. The
|
|
155
|
-
// dashboard still gets the inventory; the plan can be set later via the
|
|
156
|
-
// dashboard or an interactive `troxy tools`.
|
|
157
|
-
export function silentToolReport(detected) {
|
|
158
|
-
return detected.filter(d => d.installed).map(d => ({ slug: d.slug, installed: true, plan: 'not_sure', cycle_day: null }));
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export async function reportToolPlans(apiKey, results) {
|
|
162
|
-
if (!results || results.length === 0) return;
|
|
84
|
+
if (installed.length === 0) return;
|
|
163
85
|
try {
|
|
164
|
-
await api.reportToolPlans(apiKey,
|
|
86
|
+
await api.reportToolPlans(apiKey, installed.map(d => ({ slug: d.slug, installed: true })));
|
|
165
87
|
} catch (err) {
|
|
166
|
-
// Non-fatal: a failed tool
|
|
167
|
-
console.error(`\n Could not save AI tool info: ${err.message} (you can
|
|
88
|
+
// Non-fatal: a failed tool report must never break `troxy init`.
|
|
89
|
+
console.error(`\n Could not save AI tool info: ${err.message} (you can retry later with "troxy tools").`);
|
|
168
90
|
}
|
|
169
91
|
}
|
|
170
92
|
|
|
171
|
-
// The full flow, shared by `troxy init` (
|
|
172
|
-
|
|
93
|
+
// The full flow, shared by `troxy init` (a quiet inventory step) and
|
|
94
|
+
// `troxy tools` (an explicit re-scan). No prompting - detection is a fact,
|
|
95
|
+
// not a question - so `interactive` no longer changes behavior; kept as a
|
|
96
|
+
// parameter so callers don't need to change, in case a future non-detection
|
|
97
|
+
// step needs the distinction.
|
|
98
|
+
export async function runToolDetection(apiKey) {
|
|
173
99
|
const detected = detectAiTools();
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const results = (interactive && process.stdin.isTTY)
|
|
178
|
-
? await promptForToolPlans(detected)
|
|
179
|
-
: silentToolReport(detected);
|
|
180
|
-
await reportToolPlans(apiKey, results);
|
|
100
|
+
if (detected.every(d => !d.installed)) return;
|
|
101
|
+
await reportToolPlans(apiKey, detected);
|
|
181
102
|
}
|