toolstile 0.2.0 → 0.2.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/README.md +3 -1
- package/dist/api.d.ts +5 -0
- package/dist/commands.d.ts +1 -0
- package/dist/commands.js +17 -7
- package/dist/prompt.d.ts +5 -1
- package/dist/prompt.js +18 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -118,7 +118,9 @@ npx toolstile mcp setup
|
|
|
118
118
|
```
|
|
119
119
|
|
|
120
120
|
`auth login` prints a link and a code. Open the link on any device with a
|
|
121
|
-
browser (your laptop or phone)
|
|
121
|
+
browser (your laptop or phone) and sign in. The page fills in the code and this
|
|
122
|
+
machine's name; edit the name if you like, check the code matches and approve.
|
|
123
|
+
The machine appears under that name in the Toolstile dashboard. Nothing has to
|
|
122
124
|
reach the machine you are signing in, so it works over SSH, in containers and on
|
|
123
125
|
servers without a browser.
|
|
124
126
|
|
package/dist/api.d.ts
CHANGED
|
@@ -15,6 +15,11 @@ export interface Account {
|
|
|
15
15
|
name: string;
|
|
16
16
|
expiresAt: string;
|
|
17
17
|
workspaces: Workspace[];
|
|
18
|
+
/** The device named when this sign-in was approved. */
|
|
19
|
+
device?: {
|
|
20
|
+
id: string;
|
|
21
|
+
label: string;
|
|
22
|
+
} | null;
|
|
18
23
|
}
|
|
19
24
|
export interface DeviceCode {
|
|
20
25
|
device_code: string;
|
package/dist/commands.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export interface Context {
|
|
|
15
15
|
stderr: NodeJS.WritableStream;
|
|
16
16
|
style: Style;
|
|
17
17
|
sleep?: (ms: number) => Promise<void>;
|
|
18
|
+
hostname?: () => string;
|
|
18
19
|
openBrowser?: (url: string) => void;
|
|
19
20
|
}
|
|
20
21
|
/** A failure explained to the user; exits 1 without a stack trace. */
|
package/dist/commands.js
CHANGED
|
@@ -41,6 +41,8 @@ function defaultOpenBrowser(ctx) {
|
|
|
41
41
|
catch { }
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
|
+
/** The name this machine suggests for itself: its hostname, without macOS's `.local`. */
|
|
45
|
+
const machineName = (ctx) => (ctx.hostname ?? hostname)().replace(/\.local$/i, "");
|
|
44
46
|
const formatCode = (code) => (code.length === 8 ? `${code.slice(0, 4)}-${code.slice(4)}` : code);
|
|
45
47
|
export async function login(ctx, options) {
|
|
46
48
|
const s = ctx.style;
|
|
@@ -48,14 +50,18 @@ export async function login(ctx, options) {
|
|
|
48
50
|
const server = normalizeServer(options.server ?? ctx.env.TOOLSTILE_URL ?? state.server);
|
|
49
51
|
const api = ctx.api(server);
|
|
50
52
|
const code = await api.deviceCode();
|
|
51
|
-
|
|
53
|
+
// The page fills in the code and this machine's name; the name can be edited there and
|
|
54
|
+
// becomes the device this machine's agents are listed under.
|
|
55
|
+
const link = new URL(code.verification_uri_complete);
|
|
56
|
+
link.searchParams.set("device_name", machineName(ctx));
|
|
57
|
+
print(ctx, `Open this link on any device with a browser and approve the sign-in:`);
|
|
52
58
|
print(ctx);
|
|
53
|
-
print(ctx, ` ${s.bold(
|
|
54
|
-
print(ctx, ` ${s.bold(formatCode(code.user_code))}`);
|
|
59
|
+
print(ctx, ` ${s.bold(link.href)}`);
|
|
55
60
|
print(ctx);
|
|
61
|
+
print(ctx, `Check that the page shows this code: ${s.bold(formatCode(code.user_code))}`);
|
|
56
62
|
print(ctx, s.dim(`Waiting for approval. The code expires in ${Math.round(code.expires_in / 60)} minutes.`));
|
|
57
63
|
if (options.browser)
|
|
58
|
-
(ctx.openBrowser ?? defaultOpenBrowser(ctx))(
|
|
64
|
+
(ctx.openBrowser ?? defaultOpenBrowser(ctx))(link.href);
|
|
59
65
|
const sleep = ctx.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
60
66
|
let interval = Math.max(code.interval, 1) * 1000;
|
|
61
67
|
const deadline = Date.now() + code.expires_in * 1000;
|
|
@@ -95,9 +101,13 @@ export async function login(ctx, options) {
|
|
|
95
101
|
}
|
|
96
102
|
state.server = server;
|
|
97
103
|
state.session = { token, email: account.email, expiresAt: account.expiresAt, server };
|
|
104
|
+
// The device named on the approval page. Agents still on an older device move to it at
|
|
105
|
+
// the next `mcp setup`.
|
|
106
|
+
if (account.device)
|
|
107
|
+
state.device = account.device;
|
|
98
108
|
saveState(state, stateEnv(ctx));
|
|
99
109
|
print(ctx);
|
|
100
|
-
print(ctx, `Signed in as ${s.bold(account.email)}. Next: ${s.bold("toolstile mcp setup")}`);
|
|
110
|
+
print(ctx, `Signed in as ${s.bold(account.email)}${account.device ? ` on ${s.bold(account.device.label)}` : ""}. Next: ${s.bold("toolstile mcp setup")}`);
|
|
101
111
|
return 0;
|
|
102
112
|
}
|
|
103
113
|
export async function logout(ctx) {
|
|
@@ -179,7 +189,7 @@ export async function setup(ctx, options) {
|
|
|
179
189
|
device ??= await api.device(session.token, {
|
|
180
190
|
workspaceId: workspace.id,
|
|
181
191
|
id: state.device?.id,
|
|
182
|
-
label: options.deviceName ?? state.device?.label ??
|
|
192
|
+
label: options.deviceName ?? state.device?.label ?? machineName(ctx),
|
|
183
193
|
rename: options.deviceName !== undefined,
|
|
184
194
|
});
|
|
185
195
|
state.device = device;
|
|
@@ -201,7 +211,7 @@ export async function setup(ctx, options) {
|
|
|
201
211
|
throw error;
|
|
202
212
|
})
|
|
203
213
|
: Promise.resolve();
|
|
204
|
-
const deviceLabel = state.device?.label ??
|
|
214
|
+
const deviceLabel = state.device?.label ?? machineName(ctx);
|
|
205
215
|
const all = targets(ctx);
|
|
206
216
|
const shown = all.filter((t) => t.detected || live(t.client) || state.agents[t.client] || enable.includes(t.client));
|
|
207
217
|
if (!shown.length) {
|
package/dist/prompt.d.ts
CHANGED
|
@@ -4,7 +4,11 @@ export interface Choice {
|
|
|
4
4
|
hint?: string;
|
|
5
5
|
checked: boolean;
|
|
6
6
|
}
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Terminal checklist: arrows (or j/k) move, space toggles, a toggles all, enter saves.
|
|
9
|
+
* Every line is fitted to the terminal width: a wrapped line takes two rows, and the redraw
|
|
10
|
+
* would then move up too few rows and leave copies of the title behind.
|
|
11
|
+
*/
|
|
8
12
|
export declare function checklist(title: string, choices: Choice[], input: ReadStream, output: WriteStream, options?: {
|
|
9
13
|
single?: boolean;
|
|
10
14
|
}): Promise<boolean[] | null>;
|
package/dist/prompt.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { emitKeypressEvents } from "node:readline";
|
|
2
|
-
/**
|
|
2
|
+
/** Shortens text to `width` columns, keeping its end (the file name of a path). */
|
|
3
|
+
function fitStart(text, width) {
|
|
4
|
+
return text.length <= width ? text : width < 2 ? "" : `…${text.slice(text.length - width + 1)}`;
|
|
5
|
+
}
|
|
6
|
+
const fitEnd = (text, width) => text.length <= width ? text : `${text.slice(0, Math.max(0, width - 1))}…`;
|
|
7
|
+
/**
|
|
8
|
+
* Terminal checklist: arrows (or j/k) move, space toggles, a toggles all, enter saves.
|
|
9
|
+
* Every line is fitted to the terminal width: a wrapped line takes two rows, and the redraw
|
|
10
|
+
* would then move up too few rows and leave copies of the title behind.
|
|
11
|
+
*/
|
|
3
12
|
export function checklist(title, choices, input, output, options = {}) {
|
|
4
13
|
const state = choices.map((c) => c.checked);
|
|
5
14
|
let cursor = Math.max(0, options.single ? state.indexOf(true) : 0);
|
|
@@ -8,14 +17,19 @@ export function checklist(title, choices, input, output, options = {}) {
|
|
|
8
17
|
const render = () => {
|
|
9
18
|
if (drawn)
|
|
10
19
|
output.write(`\x1b[${drawn}A\x1b[0J`);
|
|
20
|
+
// A spare column for glyphs some terminals draw double-width.
|
|
21
|
+
const width = Math.max(20, (output.columns || 80) - 2);
|
|
11
22
|
const lines = [
|
|
12
|
-
title,
|
|
23
|
+
fitEnd(title, width),
|
|
13
24
|
...choices.map((c, i) => {
|
|
14
25
|
const mark = options.single ? (i === cursor ? "●" : "○") : state[i] ? "◉" : "○";
|
|
15
26
|
const pointer = i === cursor ? "›" : " ";
|
|
16
|
-
|
|
27
|
+
const text = fitEnd(`${pointer} ${mark} ${c.label}`, width);
|
|
28
|
+
const room = width - text.length - 2;
|
|
29
|
+
const hint = c.hint && room >= 8 ? ` \x1b[2m${fitStart(c.hint, room)}\x1b[22m` : "";
|
|
30
|
+
return `${text}${hint}`;
|
|
17
31
|
}),
|
|
18
|
-
`\x1b[2m${help}\x1b[22m`,
|
|
32
|
+
`\x1b[2m${fitEnd(help, width)}\x1b[22m`,
|
|
19
33
|
];
|
|
20
34
|
output.write(`${lines.join("\n")}\n`);
|
|
21
35
|
drawn = lines.length;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolstile",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Audit the MCP server configs your AI agents use for plaintext secrets, unpinned packages, auto-approved tools and duplicated setups, and connect those agents to Toolstile without putting tokens in their configs.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|