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 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), sign in and approve the code. Nothing has to
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;
@@ -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
- print(ctx, `Open this page on any device with a browser, sign in to Toolstile and enter the code:`);
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(code.verification_uri)}`);
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))(code.verification_uri);
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 ?? hostname().replace(/\.local$/, ""),
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 ?? hostname().replace(/\.local$/, "");
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
- /** Terminal checklist: arrows (or j/k) move, space toggles, a toggles all, enter saves. */
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
- /** Terminal checklist: arrows (or j/k) move, space toggles, a toggles all, enter saves. */
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
- return `${pointer} ${mark} ${c.label}${c.hint ? ` \x1b[2m${c.hint}\x1b[22m` : ""}`;
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.0",
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",