@invitedlabs/cli 0.1.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
@@ -1,17 +1,17 @@
1
1
  # invited
2
2
 
3
- The `invited` command connects terminal agents and AI assistants to your Invited Identity. It is a client of Invited's MCP connector (`https://invited.you/mcp`), the same connector Claude and ChatGPT use: it sets up the connector in Claude Code, Cursor, Codex CLI and Gemini CLI, and it can sign in itself to print your public Identity in a terminal.
3
+ The `invited` command connects terminal agents and AI assistants to your Invited Identity. It is a client of Invited's MCP connector (`https://invited.you/mcp`), the same connector Claude and ChatGPT use: it sets up the connector in Claude Code, Cursor, Codex CLI and Gemini CLI, and it can log in itself to print, in a terminal, the fragments you share with it.
4
4
 
5
- It reads what your Invited Page already publishes for AI readers and, if you choose to share them, private fragments you pick on Invited's consent screen. It holds an OAuth sign-in like any other connected assistant, never a vault key, and decrypts nothing.
5
+ It reads only the fragments you choose on Invited's consent screen, for as long as you chose, like every connected assistant. It never reads your Invited Page, which is for the people and programs that visit it. It holds an OAuth log-in like any other connected assistant, never a vault key, and decrypts nothing.
6
6
 
7
7
  ## Install
8
8
 
9
9
  Node 22.13 or later. Nothing else is installed: the CLI has no dependencies and uses only Node's own modules (`fetch`, `node:crypto`, `node:http`, `node:fs`).
10
10
 
11
11
  ```sh
12
- npx @invitedlabs/cli connect claude-code # add Invited to Claude Code, which then signs in itself
13
- npx @invitedlabs/cli login # or sign this CLI in: you approve in your browser
14
- npx @invitedlabs/cli whoami # then print your public Identity
12
+ npx @invitedlabs/cli connect claude-code # add Invited to Claude Code, which then logs in itself
13
+ npx @invitedlabs/cli login # or log this CLI in: you approve in your browser
14
+ npx @invitedlabs/cli whoami # then print the fragments you share with it
15
15
  ```
16
16
 
17
17
  or install it once with `npm install --global @invitedlabs/cli` and run `invited …`.
@@ -24,19 +24,21 @@ Every command prints plain English, exits 0 when it did what was asked and 1 oth
24
24
 
25
25
  | Command | What it does |
26
26
  | --- | --- |
27
- | `invited connect <host>` | Adds Invited's MCP server to `claude-code`, `cursor`, `codex` or `gemini` as `invited`, keeping every other server in the file as it is. The host then signs in to Invited itself. `--dry-run` prints what it would write and writes nothing; `--bridge` writes the `mcp-remote` bridge instead (`npx -y mcp-remote@0.14.3 https://invited.you/mcp`, pinned to the reviewed version) for a host version without remote sign-in; `--config <file>` edits another file. |
28
- | `invited login` | Signs this CLI in to your Identity. Your browser opens Invited's consent screen ("Invited CLI wants to understand you"); approve it and the browser returns to the CLI. `--private` also asks for your private fragments: on the same screen you choose the sections and fragments to share and for how long (30 days, 7 days, 24 hours, or until you disconnect). `--no-browser` prints the address to open instead (an SSH session, say). |
29
- | `invited whoami` | Prints your public Identity as Markdown (`invited://identity`, the same text as your Page's `/identity.md`). `--json` prints `invited://identity.json`. |
30
- | `invited fragments --type <type>` | Prints your public fragments of one or more types (`get_fragments`); repeat `--type` for more. Types are keys such as `skill`, `language`, `experience`, `cuisines` or `dietaryPreferences`. `--json` prints the structured answer. |
31
- | `invited fragments --search <words>` | Prints the public fragments that match, best first (`search_fragments`). |
32
- | `invited private` | Prints the private fragments you share with this CLI (`private_fragments`) and when that sharing ends. `--type <type>` (repeatable) prints only those types; `--json` prints the structured answer. When nothing is shared (you shared none, or the sharing ended) it says so: run `invited login --private` to share again. |
33
- | `invited status` | Shows the sign-in this CLI holds: your Identity and Page, its scopes (`identity:private` when you shared private fragments with it), when it was last used and when it expires. It reads the credentials file only. |
34
- | `invited logout` | Revokes this CLI's own sign-in at Invited and deletes its credentials. `invited revoke` does the same. |
27
+ | `invited connect <host>` | Adds Invited's MCP server to `claude-code`, `cursor`, `codex` or `gemini` as `invited`, keeping every other server in the file as it is. The host then logs in to Invited itself. `--dry-run` prints what it would write and writes nothing; `--bridge` writes the `mcp-remote` bridge instead (`npx -y mcp-remote@0.14.3 https://invited.you/mcp`, pinned to the reviewed version) for a host version without remote log-in; `--config <file>` edits another file. |
28
+ | `invited login` | Logs this CLI in to your Identity. Your browser opens Invited's consent screen ("Connect your Identity?", its box naming Invited CLI): you choose the fragments it may read and for how long (30 days, 7 days, 24 hours, or until you disconnect), approve it, and the browser returns to the CLI, which says how many fragments it can read. `--no-browser` prints the address to open instead (an SSH session, say). `--private`, which 0.1.0 needed to share fragments, is still taken: every log-in asks now. |
29
+ | `invited whoami` | Prints the fragments you share with this CLI as Markdown, by section, and when that sharing ends (`invited://identity`). `--json` prints `invited://identity.json`: the same as JSON. |
30
+ | `invited fragments --type <type>` | Prints the fragments you share with this CLI of one or more types (`get_fragments`); repeat `--type` for more. Types are keys such as `skill`, `language`, `experience`, `cuisines` or `dietaryPreferences`. `--json` prints the structured answer. |
31
+ | `invited fragments --search <words>` | Prints the fragments you share with this CLI that match, best first (`search_fragments`). |
32
+ | `invited private` | Prints the fragments you share with this CLI (`private_fragments`) and when that sharing ends. `--type <type>` (repeatable) prints only those types; `--json` prints the structured answer. |
33
+ | `invited status` | Shows the log-in this CLI holds: your Identity, its scopes (`identity:private` while you share fragments with it), when it was last used and when it expires. `--json` prints the same as JSON, with `assistantsUrl`, the address of Activity's Assistants. It reads the credentials file only. |
34
+ | `invited logout` | Revokes this CLI's own log-in at Invited and deletes its credentials. `invited revoke` does the same. |
35
35
  | `invited --help`, `invited help <command>`, `invited --version` | Help and the version. |
36
36
 
37
- Where `connect` writes, and how each host signs in:
37
+ When nothing is shared with the CLI (you shared none, or the time you chose has ended), `whoami`, `fragments` and `private` say so: run `invited login` to choose again.
38
38
 
39
- | Host | File | Entry | Sign-in |
39
+ Where `connect` writes, and how each host logs in:
40
+
41
+ | Host | File | Entry | Log in |
40
42
  | --- | --- | --- | --- |
41
43
  | Claude Code | `~/.claude.json` (user scope) | `"invited": { "type": "http", "url": "https://invited.you/mcp" }` | `claude mcp login invited`, or `/mcp` in Claude Code |
42
44
  | Cursor | `~/.cursor/mcp.json` | `"invited": { "url": "https://invited.you/mcp" }` | Customize > MCP in Cursor |
@@ -45,25 +47,38 @@ Where `connect` writes, and how each host signs in:
45
47
 
46
48
  A file the CLI cannot edit safely (JSON with comments, a TOML table written inline) is left as it is, and the CLI prints the entry to add by hand. Running `connect` again changes nothing.
47
49
 
48
- ## Your sign-in
50
+ ## Your log-in
49
51
 
50
- `invited login` is OAuth 2.1 with PKCE as the pre-registered public client `invited-cli`. While you approve, the CLI listens on `http://127.0.0.1:<port>/callback`, on a port the system picks, bound to this computer only. The listener accepts one answer, the one carrying this sign-in's `state` and Invited's `iss`, and closes as soon as it arrives, or after 5 minutes.
52
+ `invited login` is OAuth 2.1 with PKCE as the pre-registered public client `invited-cli`. While you approve, the CLI listens on `http://127.0.0.1:<port>/callback`, on a port the system picks, bound to this computer only. The listener accepts one answer, the one carrying this log-in's `state` and Invited's `iss`, and closes as soon as it arrives, or after 5 minutes.
51
53
 
52
- The sign-in is kept in `~/.invited/credentials.json` (set `INVITED_CONFIG_DIR` to keep it elsewhere): the refresh token, your Identity's id, its scopes, when it was made and last used, and your Page's address. The file is written with mode 0600 in a 0700 folder (on Windows, your user profile's permissions protect it). Access tokens are never written: each command renews one in memory, and Invited issues a new refresh token every time, so the file changes after every command. Two commands at once wait for each other rather than renew the same token.
54
+ The log-in is kept in `~/.invited/credentials.json` (set `INVITED_CONFIG_DIR` to keep it elsewhere): the refresh token, your Identity's id, its scopes, and when it was made and last used (and, empty since 0.2.0, the Page address 0.1.0 kept). The file is written with mode 0600 in a 0700 folder (on Windows, your user profile's permissions protect it). Access tokens are never written: each command renews one in memory, and Invited issues a new refresh token every time, so the file changes after every command. Two commands at once wait for each other rather than renew the same token.
53
55
 
54
- A sign-in lasts 30 days from its last use, and 90 days at most; then run `invited login` again.
56
+ A log-in lasts 30 days from its last use, and 90 days at most; then run `invited login` again.
55
57
 
56
58
  ## Reads and the budget
57
59
 
58
- `whoami` and `fragments` are reads of your public Identity, and `private` of what you share privately, the same reads a connected assistant makes. Each one appears in your access log in Settings, under Invited CLI. Invited allows each connection 60 reads a minute; every Invited CLI signed in to your Identity shares one connection, so they share that budget. Past it, the CLI says how many seconds to wait.
60
+ `whoami`, `fragments` and `private` are reads of what you share with the CLI, the same reads a connected assistant makes. Each one appears in your access log in Activity, Reads, as Invited CLI. Invited allows each connection 60 reads a minute; every Invited CLI logged in to your Identity shares one connection, so they share that budget. Past it, the CLI says how many seconds to wait.
59
61
 
60
- ## Private fragments
62
+ ## What it reads
61
63
 
62
- Private fragments reach the CLI only when you share them on the consent screen (`invited login --private`). While they are shared, Invited's server can read those fragments to pass them to the CLI; with your vault on, everything else stays sealed. Settings, Connected assistants shows what is shared and until when, and Disconnect ends it at once.
64
+ Fragments reach the CLI only when you share them on the consent screen (`invited login`), and only for as long as you chose. While they are shared, Invited's server can read those fragments to pass them to the CLI; with your vault on, everything else stays sealed. Activity, Assistants shows what is shared and until when, and Disconnect ends it at once.
63
65
 
64
66
  ## Removing it
65
67
 
66
- 1. `invited logout` revokes the sign-in and deletes `~/.invited/credentials.json` (the folder `~/.invited` can then go too).
67
- 2. To remove the connection itself, use Settings, Connected assistants: https://invited.you/app/settings. Its reads stay in your access log for 12 months. The CLI can end only its own sign-in; other assistants are disconnected there.
68
+ 1. `invited logout` revokes the log-in and deletes `~/.invited/credentials.json` (the folder `~/.invited` can then go too).
69
+ 2. To remove the connection itself, use Activity, Assistants: https://invited.you/app/activity?tab=assistants. Its reads stay in your access log for 3 months. The CLI can end only its own log-in; other assistants are disconnected there.
68
70
  3. Remove the `invited` entry from each host you connected (`claude mcp remove invited`, or delete it from the file listed above).
69
71
  4. `npm uninstall --global @invitedlabs/cli` if you installed it.
72
+
73
+ ## Changes in 0.2.1
74
+
75
+ - After you approve or cancel the log-in in your browser, the CLI sends the browser to Invited's own page for it, in the same look as invited.you's log-in pages. The page says only whether it connected; the code stays with the CLI.
76
+
77
+ ## Changes in 0.2.0
78
+
79
+ - The CLI reads only the fragments you share with it, never your Page, as every connected assistant does since 8 November 2026. `invited login` always asks you to choose them (`--private` is still taken); `whoami` and `fragments` print them; `status` no longer shows a Page. 0.1.0 reads only after `invited login --private`, and its `invited fragments` answers "Invited answered in a form this CLI does not know.": update to 0.2.0.
80
+
81
+ - Connected assistants and their reads are in Activity now, not Settings: `invited status`, the messages and this README send you to Activity, Assistants (https://invited.you/app/activity?tab=assistants).
82
+ - `invited status --json` names that address `assistantsUrl`. In 0.1.0 it was `settingsUrl`, which is gone: a script that read it reads `assistantsUrl` instead.
83
+ - Your access log keeps each read for 3 months.
84
+ - "Log in" and "log out" in every message, as on invited.you. The commands are still `invited login` and `invited logout`.
package/dist/commands.js CHANGED
@@ -4,8 +4,10 @@ import { duration, formatTime, plural } from './format.js';
4
4
  import { listenForCallback } from './loopback.js';
5
5
  import { McpError, mcpRequest, ScopeRefused } from './mcp.js';
6
6
  import { authorizationUrl, CLIENT_ID, discover, exchangeCode, GrantRefused, newState, pkcePair, PRIVATE_SCOPE, PUBLIC_SCOPE, refreshTokens, revokeRefreshToken, } from './oauth.js';
7
- export const settingsUrl = (origin) => `${origin}/app/settings`;
8
- export const settingsLine = (origin) => `Manage every connected assistant in Settings at ${settingsUrl(origin)}.`;
7
+ /** Activity's Assistants, where every connected assistant is listed and disconnected (owner, 3 November 2026: what
8
+ * sent members to Settings for assistants and the access log says Activity; Settings keeps one row linking there). */
9
+ export const assistantsUrl = (origin) => `${origin}/app/activity?tab=assistants`;
10
+ export const assistantsLine = (origin) => `Manage every connected assistant in Activity at ${assistantsUrl(origin)}.`;
9
11
  const iso = (time) => new Date(time).toISOString();
10
12
  const notConnected = (origin) => new CliError(`The Invited CLI is not connected to ${origin}. Run \`invited login\` to connect it.`);
11
13
  /** Access tokens for this command: one renewal (refresh token rotation)
@@ -27,7 +29,7 @@ export function accessTokens(ctx, primed) {
27
29
  }
28
30
  catch (error) {
29
31
  if (error instanceof GrantRefused)
30
- throw new CliError(`Invited did not renew this CLI's sign-in: ${error.description} Run \`invited login\` to connect again.`);
32
+ throw new CliError(`Invited did not renew this CLI's log-in: ${error.description} Run \`invited login\` to connect again.`);
31
33
  throw error;
32
34
  }
33
35
  file.connections[ctx.origin] = {
@@ -43,29 +45,68 @@ export function accessTokens(ctx, primed) {
43
45
  return current.token;
44
46
  };
45
47
  }
46
- /** Remember the Page a read named, for `invited status`. */
47
- async function rememberPage(ctx, pageUrl) {
48
- if (typeof pageUrl !== 'string' || !/^https?:\/\//.test(pageUrl))
49
- return;
50
- await ctx.store.locked(async () => {
51
- const file = await ctx.store.read();
52
- const connection = file.connections[ctx.origin];
53
- if (!connection)
54
- return;
55
- file.connections[ctx.origin] = { ...connection, pageUrl, pageReadAt: iso(ctx.now()) };
56
- await ctx.store.write(file);
57
- });
48
+ /** Nothing is shared with the CLI now (it was given none, or the sharing ended): the member chooses again. */
49
+ export const NOTHING_SHARED = 'Nothing is shared with the CLI right now. Run `invited login` to choose the fragments it may read.';
50
+ /** An answer read as every read writes it, or the CLI says it does not know the form. */
51
+ function inScopeOf(value) {
52
+ const data = value;
53
+ if (typeof data !== 'object' ||
54
+ data === null ||
55
+ !(data.expiresAt === null || typeof data.expiresAt === 'string') ||
56
+ !Array.isArray(data.fragments))
57
+ throw new CliError('Invited answered in a form this CLI does not know.');
58
+ return data;
59
+ }
60
+ /** A read's refusal in the CLI's words: nothing shared now is the way to share again. */
61
+ function refused(error) {
62
+ if (error instanceof ScopeRefused && error.scope === PRIVATE_SCOPE)
63
+ throw new CliError(NOTHING_SHARED);
64
+ if (error instanceof McpError)
65
+ throw new CliError(error.message);
66
+ throw error;
67
+ }
68
+ /** One tool call: its answer, from its structured content or the JSON its text carries. A tool error is the server's
69
+ * explanation (an unknown type). The Profile photo's image, after the JSON, is not printed: a terminal shows its
70
+ * answer as any other, the path the JSON holds on its first line and its caption, if any, under it. */
71
+ async function callTool(ctx, name, args) {
72
+ let result;
73
+ try {
74
+ result = await mcpRequest(ctx.fetch, ctx.origin, accessTokens(ctx), 'tools/call', { name, arguments: args });
75
+ }
76
+ catch (error) {
77
+ refused(error);
78
+ }
79
+ const blocks = Array.isArray(result.content) ? result.content : [];
80
+ const texts = blocks.flatMap(item => (item && typeof item === 'object' && 'text' in item ? [String(item.text)] : []));
81
+ if (result.isError === true)
82
+ throw new CliError(texts.join('\n') || 'Invited could not read those fragments.');
83
+ if (result.structuredContent !== undefined)
84
+ return inScopeOf(result.structuredContent);
85
+ try {
86
+ return inScopeOf(JSON.parse(texts.findLast(text => text.trimStart().startsWith('{')) ?? ''));
87
+ }
88
+ catch (error) {
89
+ if (error instanceof CliError)
90
+ throw error;
91
+ throw new CliError('Invited answered in a form this CLI does not know.');
92
+ }
58
93
  }
59
94
  async function readResource(ctx, token, uri) {
60
95
  const result = await mcpRequest(ctx.fetch, ctx.origin, token, 'resources/read', { uri });
61
96
  const content = (Array.isArray(result.contents) ? result.contents[0] : null);
62
97
  if (typeof content?.text !== 'string')
63
98
  throw new CliError('Invited answered without the document.');
64
- return { text: content.text, pageUrl: content._meta?.['you.invited/pageUrl'] };
99
+ return content.text;
65
100
  }
101
+ /** How long the sharing lasts, in words. An edit never ends it (owner, 8 November 2026: under Current and Specify an
102
+ * edit updates the shared copy, as under Automatic). */
103
+ const lasting = (ctx, expiresAt) => expiresAt ? `until ${formatTime(expiresAt, ctx.timeZone)}` : 'until you disconnect it in Activity';
104
+ /** A fragment as the CLI prints it: its name, then its answer, a line at a time. */
105
+ const shownFragment = (ctx, fragment) => `${ctx.style.bold(fragment.label)}: ${fragment.answer.replace(/\r?\n/g, '\n ')}`;
66
106
  // login ----------------------------------------------------------------------
67
- /** The step-up: nothing private is shared with the CLI now. */
68
- export const NOTHING_SHARED = 'Nothing is shared with the CLI right now. Run `invited login --private` to share fragments.';
107
+ /** Every log-in asks for identity:private (owner, 8 November 2026): the connector serves only the fragments the
108
+ * member chooses on the consent screen, so a log-in with identity:public alone reads nothing. `--private`, 0.1.0's way
109
+ * to ask for it, is still taken. */
69
110
  export async function login(ctx, options) {
70
111
  const { noBrowser } = options;
71
112
  const endpoints = await discover(ctx.fetch, ctx.origin);
@@ -79,11 +120,8 @@ export async function login(ctx, options) {
79
120
  });
80
121
  let tokens;
81
122
  try {
82
- const scopes = options.private ? [PUBLIC_SCOPE, PRIVATE_SCOPE] : [PUBLIC_SCOPE];
83
- const url = authorizationUrl(endpoints, callback.redirectUri, challenge, state, scopes);
84
- ctx.out(options.private
85
- ? 'To connect the Invited CLI to your Identity, approve it in your browser and choose the private fragments to share with it.\n'
86
- : 'To connect the Invited CLI to your Identity, approve it in your browser.\n');
123
+ const url = authorizationUrl(endpoints, callback.redirectUri, challenge, state, [PUBLIC_SCOPE, PRIVATE_SCOPE]);
124
+ ctx.out('To connect the Invited CLI to your Identity, approve it in your browser and choose the fragments it may read.\n');
87
125
  const opened = !noBrowser && (await ctx.openBrowser(url));
88
126
  ctx.out((opened
89
127
  ? 'Your browser should open. If it does not, open this address:\n'
@@ -97,7 +135,7 @@ export async function login(ctx, options) {
97
135
  }
98
136
  catch (error) {
99
137
  if (error instanceof GrantRefused)
100
- throw new CliError(`Invited did not complete the sign-in: ${error.description} Run \`invited login\` again.`);
138
+ throw new CliError(`Invited did not finish logging you in: ${error.description} Run \`invited login\` again.`);
101
139
  throw error;
102
140
  }
103
141
  }
@@ -116,6 +154,7 @@ export async function login(ctx, options) {
116
154
  refreshToken: tokens.refreshToken,
117
155
  connectedAt: now,
118
156
  lastUsedAt: now,
157
+ // No read names a Page since 8 November 2026; kept, null, so an older CLI still reads the file.
119
158
  pageUrl: null,
120
159
  pageReadAt: null,
121
160
  };
@@ -126,166 +165,102 @@ export async function login(ctx, options) {
126
165
  await ctx.store.write(file);
127
166
  return before;
128
167
  });
129
- // A sign-in this one replaces ends now rather than in 30 days.
168
+ // A log-in this one replaces ends now rather than in 30 days.
130
169
  if (previous)
131
170
  await revokeRefreshToken(ctx.fetch, previous).catch(() => undefined);
132
- // One read names the member's Page; the connection stands either way.
133
- let read = 'Connected. The Invited CLI can read your public Identity.';
134
- try {
135
- const { pageUrl } = await readResource(ctx, accessTokens(ctx, { token: tokens.accessToken, expiresAt: tokens.accessExpiresAt }), 'invited://identity.json');
136
- if (typeof pageUrl === 'string') {
137
- read = `Connected. The Invited CLI can read the public Identity on ${pageUrl}.`;
138
- await rememberPage(ctx, pageUrl);
171
+ // One read says what it may read; the connection stands either way. Shared nothing, it reads nothing: since migration
172
+ // 190 that read is an empty list, before it a refusal, and the CLI says the same of both.
173
+ const none = 'Connected, but you shared no fragments with it, so it can read nothing. Run `invited login` again to choose some.';
174
+ let read = none, reads = false;
175
+ if (tokens.scopes.includes(PRIVATE_SCOPE))
176
+ try {
177
+ const shared = inScopeOf(JSON.parse(await readResource(ctx, accessTokens(ctx, { token: tokens.accessToken, expiresAt: tokens.accessExpiresAt }), 'invited://identity.json')));
178
+ reads = shared.fragments.length > 0;
179
+ read = reads
180
+ ? `Connected. The Invited CLI can read ${plural(shared.fragments.length, 'fragment')} you chose to share with it, ${lasting(ctx, shared.expiresAt)}.`
181
+ : none;
182
+ }
183
+ catch (error) {
184
+ if (error instanceof ScopeRefused)
185
+ read = none;
186
+ else if (error instanceof CliError)
187
+ read = `Connected, but your fragments could not be read just now: ${error.message}`;
188
+ else if (error instanceof SyntaxError)
189
+ read = 'Connected, but Invited answered in a form this CLI does not know.';
190
+ else
191
+ throw error;
139
192
  }
140
- }
141
- catch (error) {
142
- if (!(error instanceof CliError))
143
- throw error;
144
- read = `Connected, but your Page could not be read just now: ${error.message}`;
145
- }
146
- const shares = tokens.scopes.includes(PRIVATE_SCOPE);
147
193
  ctx.out(`${read}\n` +
148
- (shares
149
- ? 'It can also read the private fragments you chose to share with it, until that sharing ends.\n'
150
- : options.private
151
- ? 'You shared no private fragments with it. Run `invited login --private` again to share some.\n'
152
- : '') +
153
- `Its sign-in is saved in ${ctx.store.path} and lasts 30 days from its last use (90 days at most).\n` +
154
- (shares ? 'Try: invited private\n' : 'Try: invited whoami\n'));
194
+ `Its log-in is saved in ${ctx.store.path} and lasts 30 days from its last use (90 days at most).\n` +
195
+ (reads ? 'Try: invited whoami\n' : ''));
155
196
  }
156
197
  // whoami ---------------------------------------------------------------------
198
+ /** The fragments the member lets the CLI read, as Markdown (invited://identity), or as JSON (invited://identity.json). */
157
199
  export async function whoami(ctx, json) {
158
- const uri = json ? 'invited://identity.json' : 'invited://identity';
159
- let read;
200
+ let text;
160
201
  try {
161
- read = await readResource(ctx, accessTokens(ctx), uri);
202
+ text = await readResource(ctx, accessTokens(ctx), json ? 'invited://identity.json' : 'invited://identity');
162
203
  }
163
204
  catch (error) {
164
- if (error instanceof McpError)
165
- throw new CliError(error.message);
166
- throw error;
205
+ refused(error);
167
206
  }
168
- ctx.out(read.text.endsWith('\n') ? read.text : read.text + '\n');
169
- await rememberPage(ctx, read.pageUrl);
207
+ ctx.out(text.endsWith('\n') ? text : text + '\n');
170
208
  }
209
+ // fragments ------------------------------------------------------------------
210
+ /** The fragments the member lets the CLI read, of some types (get_fragments) or that match some words, best first
211
+ * (search_fragments). */
171
212
  export async function fragments(ctx, options) {
172
213
  if (options.types.length && options.search !== undefined)
173
214
  throw new CliError('Use --type or --search, not both.');
174
215
  if (!options.types.length && options.search === undefined)
175
216
  throw new CliError('Name a fragment type with --type <type> (repeat it for more), or search with --search <words>. For example: invited fragments --type skill');
176
- const call = options.types.length
177
- ? { name: 'get_fragments', arguments: { types: [...new Set(options.types)] } }
178
- : { name: 'search_fragments', arguments: { query: options.search } };
179
- let result;
180
- try {
181
- result = await mcpRequest(ctx.fetch, ctx.origin, accessTokens(ctx), 'tools/call', call);
182
- }
183
- catch (error) {
184
- if (error instanceof McpError)
185
- throw new CliError(error.message);
186
- throw error;
187
- }
188
- const text = Array.isArray(result.content)
189
- ? result.content
190
- .map(item => (item && typeof item === 'object' && 'text' in item ? String(item.text) : ''))
191
- .join('\n')
192
- : '';
193
- // A tool error is the server's explanation (an unknown type, nothing published).
194
- if (result.isError === true)
195
- throw new CliError(text || 'Invited could not read those fragments.');
196
- let data = result.structuredContent;
197
- if (!data) {
198
- try {
199
- data = JSON.parse(text);
200
- }
201
- catch {
202
- data = undefined;
203
- }
204
- }
205
- if (typeof data?.pageUrl !== 'string' || !Array.isArray(data.fragments))
206
- throw new CliError('Invited answered in a form this CLI does not know.');
207
- await rememberPage(ctx, data.pageUrl);
217
+ const types = [...new Set(options.types)];
218
+ const data = types.length
219
+ ? await callTool(ctx, 'get_fragments', { types })
220
+ : await callTool(ctx, 'search_fragments', { query: options.search });
208
221
  if (options.json) {
209
222
  ctx.out(JSON.stringify(data, null, 2) + '\n');
210
223
  return;
211
224
  }
212
225
  const list = data.fragments;
213
- const what = options.types.length
214
- ? `of type ${options.types.join(', ')}`
226
+ const what = types.length
227
+ ? `of type ${types.join(', ')}`
215
228
  : `matching "${options.search}"${list.length > 1 ? ', best first' : ''}`;
216
229
  if (!list.length) {
217
- ctx.out(`No public fragments ${what} on ${data.pageUrl}\n`);
230
+ ctx.out(`No shared fragments ${what}.\n`);
218
231
  return;
219
232
  }
220
- const lines = [`${plural(list.length, 'public fragment')} ${what}, from ${data.pageUrl}`, ''];
221
- for (const fragment of list) {
222
- lines.push(`${ctx.style.bold(fragment.label)}: ${fragment.answer}`);
223
- if (fragment.question)
224
- lines.push(` Answers: ${fragment.question}`);
225
- lines.push(` ${fragment.url}`);
226
- }
233
+ const lines = [`${plural(list.length, 'shared fragment')} ${what}`, ''];
234
+ for (const fragment of list)
235
+ lines.push(shownFragment(ctx, fragment));
227
236
  ctx.out(lines.join('\n') + '\n');
228
237
  }
229
- /** private_fragments: the private fragments the member shares with the CLI
230
- * (step 4b), and when that sharing ends. Nothing shared (none was, it ended,
231
- * or the sign-in has only the public scope) is the step-up message. */
238
+ // private --------------------------------------------------------------------
239
+ /** private_fragments: the fragments the member shares with the CLI (step 4b), and when that sharing ends. Nothing
240
+ * shared (none was, it ended, or the log-in has only the public scope) is the message to choose again. */
232
241
  export async function privateFragments(ctx, options) {
233
242
  const types = [...new Set(options.types)];
234
- let result;
235
- try {
236
- result = await mcpRequest(ctx.fetch, ctx.origin, accessTokens(ctx), 'tools/call', {
237
- name: 'private_fragments',
238
- arguments: types.length ? { types } : {},
239
- });
240
- }
241
- catch (error) {
242
- if (error instanceof ScopeRefused && error.scope === PRIVATE_SCOPE)
243
- throw new CliError(NOTHING_SHARED);
244
- if (error instanceof McpError)
245
- throw new CliError(error.message);
246
- throw error;
247
- }
248
- const text = Array.isArray(result.content)
249
- ? result.content
250
- .map(item => (item && typeof item === 'object' && 'text' in item ? String(item.text) : ''))
251
- .join('\n')
252
- : '';
253
- // A tool error is the server's explanation (an unknown type).
254
- if (result.isError === true)
255
- throw new CliError(text || 'Invited could not read those fragments.');
256
- let data = result.structuredContent;
257
- if (!data) {
258
- try {
259
- data = JSON.parse(text);
260
- }
261
- catch {
262
- data = undefined;
263
- }
264
- }
265
- if (!data || !(data.expiresAt === null || typeof data.expiresAt === 'string') || !Array.isArray(data.fragments))
266
- throw new CliError('Invited answered in a form this CLI does not know.');
243
+ const data = await callTool(ctx, 'private_fragments', types.length ? { types } : {});
267
244
  if (options.json) {
268
245
  ctx.out(JSON.stringify(data, null, 2) + '\n');
269
246
  return;
270
247
  }
271
248
  const list = data.fragments;
272
- const until = typeof data.expiresAt === 'string'
273
- ? `until ${formatTime(data.expiresAt, ctx.timeZone)}`
274
- : 'until you disconnect it in Settings or edit the fragment';
249
+ const until = lasting(ctx, data.expiresAt);
275
250
  const what = types.length ? ` of type ${types.join(', ')}` : '';
276
251
  if (!list.length) {
277
- ctx.out(`No private fragments${what} are shared with the Invited CLI. Its sharing lasts ${until}.\n`);
252
+ ctx.out(`No fragments${what} are shared with the Invited CLI. Its sharing lasts ${until}.\n`);
278
253
  return;
279
254
  }
280
- const lines = [`${plural(list.length, 'private fragment')}${what} shared with the Invited CLI ${until}.`, ''];
255
+ const lines = [`${plural(list.length, 'fragment')}${what} shared with the Invited CLI ${until}.`, ''];
281
256
  for (const fragment of list)
282
- lines.push(`${ctx.style.bold(fragment.label)}: ${fragment.answer.replace(/\r?\n/g, '\n ')}`);
283
- lines.push('', settingsLine(ctx.origin));
257
+ lines.push(shownFragment(ctx, fragment));
258
+ lines.push('', assistantsLine(ctx.origin));
284
259
  ctx.out(lines.join('\n') + '\n');
285
260
  }
286
261
  // status ---------------------------------------------------------------------
287
262
  /** What this CLI holds, from its credentials file alone: no network. Listing
288
- * other assistants needs the member's browser session (Settings). */
263
+ * other assistants needs the member's browser session (Activity). */
289
264
  export async function status(ctx, json) {
290
265
  const file = await ctx.store.read();
291
266
  const connection = file.connections[ctx.origin];
@@ -304,6 +279,7 @@ export async function status(ctx, json) {
304
279
  origin: connection.origin,
305
280
  clientId: connection.clientId,
306
281
  identityId: connection.identityId,
282
+ // No read names a Page since 8 November 2026: null unless an older CLI's log-in named one.
307
283
  pageUrl: connection.pageUrl,
308
284
  pageReadAt: connection.pageReadAt,
309
285
  scopes: connection.scopes,
@@ -312,28 +288,19 @@ export async function status(ctx, json) {
312
288
  expiresAt: iso(expiresAt),
313
289
  expiresAtLatest: iso(latest),
314
290
  credentialsFile: ctx.store.path,
315
- settingsUrl: settingsUrl(ctx.origin),
291
+ assistantsUrl: assistantsUrl(ctx.origin),
316
292
  }, null, 2) + '\n');
317
293
  return expired ? 1 : 0;
318
294
  }
319
295
  const time = (value) => formatTime(value, ctx.timeZone);
296
+ const reads = connection.scopes.includes(PRIVATE_SCOPE);
320
297
  // The public scope first, as every connection holds it.
321
298
  const scopes = [...connection.scopes]
322
299
  .sort((a, b) => Number(b === PUBLIC_SCOPE) - Number(a === PUBLIC_SCOPE))
323
- .map(scope => scope === PUBLIC_SCOPE
324
- ? 'identity:public (your public Identity)'
325
- : scope === PRIVATE_SCOPE
326
- ? 'identity:private (the private fragments you share with it)'
327
- : scope)
300
+ .map(scope => (scope === PRIVATE_SCOPE ? 'identity:private (the fragments you chose to share with it)' : scope))
328
301
  .join(', ');
329
302
  const rows = [
330
303
  ['Identity', connection.identityId],
331
- [
332
- 'Page',
333
- connection.pageUrl
334
- ? `${connection.pageUrl} (as of ${time(connection.pageReadAt ?? connection.lastUsedAt)})`
335
- : 'not read yet: run `invited whoami`',
336
- ],
337
304
  ['Scopes', scopes],
338
305
  ['Connected', time(connection.connectedAt)],
339
306
  ['Last used', time(connection.lastUsedAt)],
@@ -346,20 +313,20 @@ export async function status(ctx, json) {
346
313
  ['Credentials', ctx.store.path],
347
314
  ];
348
315
  ctx.out([
349
- ctx.style.bold(`The Invited CLI is signed in to ${connection.origin} (client ${connection.clientId}).`),
316
+ ctx.style.bold(`The Invited CLI is logged in to ${connection.origin} (client ${connection.clientId}).`),
350
317
  ...rows.map(([label, value]) => ` ${label.padEnd(13)}${value}`),
351
- connection.scopes.includes(PRIVATE_SCOPE)
352
- ? 'Run `invited private` to see the private fragments shared with it and when that sharing ends.'
353
- : 'It reads no private fragments. Run `invited login --private` to share some with it.',
354
- 'The CLI can end only its own sign-in (`invited logout`).',
355
- settingsLine(ctx.origin),
318
+ reads
319
+ ? 'Run `invited whoami` to see the fragments it reads and when that sharing ends.'
320
+ : 'It reads nothing: you shared no fragments with it. Run `invited login` to choose some.',
321
+ 'The CLI can end only its own log-in (`invited logout`).',
322
+ assistantsLine(ctx.origin),
356
323
  ].join('\n') + '\n');
357
324
  return expired ? 1 : 0;
358
325
  }
359
326
  // logout ---------------------------------------------------------------------
360
327
  export async function logout(ctx, target) {
361
328
  if (target !== undefined && !['invited-cli', 'invited', 'cli'].includes(target))
362
- throw new CliError(`The Invited CLI can end only its own sign-in, not ${target}'s. Disconnect ${target} in Settings at ${settingsUrl(ctx.origin)}.`);
329
+ throw new CliError(`The Invited CLI can end only its own log-in, not ${target}'s. Disconnect ${target} in Activity at ${assistantsUrl(ctx.origin)}.`);
363
330
  const ended = await ctx.store.locked(async () => {
364
331
  const file = await ctx.store.read();
365
332
  const connection = file.connections[ctx.origin];
@@ -371,7 +338,7 @@ export async function logout(ctx, target) {
371
338
  return true;
372
339
  });
373
340
  ctx.out((ended
374
- ? `Revoked the Invited CLI's sign-in to ${ctx.origin} and deleted its credentials from ${ctx.store.path}.\n`
375
- : `The Invited CLI was not signed in to ${ctx.origin}; nothing to revoke.\n`) +
376
- `Other assistants stay connected. ${settingsLine(ctx.origin)}\n`);
341
+ ? `Revoked the Invited CLI's log-in to ${ctx.origin} and deleted its credentials from ${ctx.store.path}.\n`
342
+ : `The Invited CLI was not logged in to ${ctx.origin}; nothing to revoke.\n`) +
343
+ `Other assistants stay connected. ${assistantsLine(ctx.origin)}\n`);
377
344
  }
package/dist/connect.js CHANGED
@@ -30,7 +30,7 @@ const HOST = {
30
30
  scope: 'user scope: every project',
31
31
  native: url => ({ type: 'http', url }),
32
32
  bridged: bridge => ({ type: 'stdio', ...bridge }),
33
- signIn: 'Claude Code signs in to Invited itself: run `claude mcp login invited` (or /mcp inside Claude Code) and approve in your browser.',
33
+ signIn: 'Claude Code logs in to Invited itself: run `claude mcp login invited` (or /mcp inside Claude Code) and approve in your browser.',
34
34
  source: 'https://code.claude.com/docs/en/mcp',
35
35
  },
36
36
  cursor: {
@@ -40,7 +40,7 @@ const HOST = {
40
40
  scope: 'global: every project',
41
41
  native: url => ({ url }),
42
42
  bridged: bridge => ({ type: 'stdio', ...bridge }),
43
- signIn: 'Cursor signs in to Invited itself: in Cursor, open Customize > MCP, sign in to invited and approve in your browser.',
43
+ signIn: 'Cursor logs in to Invited itself: in Cursor, open Customize > MCP, log in to invited and approve in your browser.',
44
44
  source: 'https://cursor.com/docs/mcp',
45
45
  },
46
46
  codex: {
@@ -50,7 +50,7 @@ const HOST = {
50
50
  scope: 'user configuration, shared by the Codex CLI, IDE extension and app',
51
51
  native: url => ({ url }),
52
52
  bridged: bridge => ({ ...bridge }),
53
- signIn: 'Codex signs in to Invited itself: run `codex mcp login invited` and approve in your browser.',
53
+ signIn: 'Codex logs in to Invited itself: run `codex mcp login invited` and approve in your browser.',
54
54
  source: 'https://developers.openai.com/codex/mcp',
55
55
  },
56
56
  gemini: {
@@ -60,7 +60,7 @@ const HOST = {
60
60
  scope: 'user settings: every project',
61
61
  native: url => ({ httpUrl: url }),
62
62
  bridged: bridge => ({ ...bridge }),
63
- signIn: 'Gemini CLI signs in to Invited itself: start gemini and approve in your browser when it connects (/mcp auth invited signs in again).',
63
+ signIn: 'Gemini CLI logs in to Invited itself: start gemini and approve in your browser when it connects (/mcp auth invited logs in again).',
64
64
  source: 'https://geminicli.com/docs/tools/mcp-server/',
65
65
  },
66
66
  };
package/dist/loopback.js CHANGED
@@ -1,51 +1,27 @@
1
- import { randomBytes } from 'node:crypto';
2
1
  import { createServer } from 'node:http';
3
2
  import { duration } from './format.js';
4
- const escape = (text) => text.replace(/[&<>"']/g, character => `&#${character.charCodeAt(0)};`);
5
- /** The Invited lockup (public/brand/invited-lockup.svg: the loading-screen
6
- * proportions, mark larger than wordmark), drawn in the page's ink. */
7
- const LOCKUP = '<svg viewBox="0 0 440 160" fill="none" xmlns="http://www.w3.org/2000/svg" aria-label="Invited" role="img">' +
8
- '<path d="M60.3125 69.9999H70.5454C72.0037 69.9999 73.2485 69.4843 74.2797 68.4531L107.632 35.1007C108.663 34.0695 109.695 34.0695 110.726 35.1007L121.774 46.1492C122.805 47.1804 122.805 48.2116 121.774 49.2428L92.5639 78.4531C91.5327 79.4843 91.5327 80.5155 92.5639 81.5467L121.774 110.757C122.805 111.788 122.805 112.819 121.774 113.851L110.726 124.899C109.695 125.93 108.663 125.93 107.632 124.899L74.2797 91.5467C73.2485 90.5155 72.0037 89.9999 70.5454 89.9999H60.3125C58.8542 89.9999 58.125 89.2708 58.125 87.8124V72.1874C58.125 70.7291 58.8542 69.9999 60.3125 69.9999Z" fill="currentColor"/>' +
9
- '<path d="M49.6875 66.875H35.3125C34.1044 66.875 33.125 67.8544 33.125 69.0625V90.9375C33.125 92.1456 34.1044 93.125 35.3125 93.125H49.6875C50.8956 93.125 51.875 92.1456 51.875 90.9375V69.0625C51.875 67.8544 50.8956 66.875 49.6875 66.875Z" fill="currentColor"/>' +
10
- '<path d="M163.28 106H152.4V49.04H163.28V106ZM205.688 106H195.368V80.72C195.368 75.12 193.368 72.32 188.328 72.32C182.888 72.32 179.368 76.24 179.368 82.96V106H169.048V64.72H178.888V71.44C181.128 67.04 184.808 63.92 191.528 63.92C200.968 63.92 205.688 68.72 205.688 78.72V106ZM233.395 106H220.915L205.635 64.72H216.435L227.155 96.96L237.875 64.72H248.675L233.395 106ZM260.896 106H250.576V64.72H260.896V106ZM255.776 58.56C251.936 58.56 249.776 56.24 249.776 53.2C249.776 50.16 251.936 47.84 255.776 47.84C259.616 47.84 261.696 50.16 261.696 53.2C261.696 56.24 259.616 58.56 255.776 58.56ZM281.988 106.4C272.628 106.4 269.508 103.52 269.508 94.4V72.08H263.028V64.72H269.508V52.24H279.828V64.72H288.788V72.08H279.828V94.8C279.828 98 281.188 99.12 284.468 99.12L288.788 99.04V106C287.108 106.16 283.828 106.4 281.988 106.4ZM309.745 106.8C296.305 106.8 288.785 98.4 288.785 85.36C288.785 72.64 295.985 63.92 309.265 63.92C322.625 63.92 329.825 72.72 329.825 84.88C329.825 85.76 329.745 86.56 329.665 87.52H299.105C299.425 95.92 303.425 99.76 309.745 99.76C314.865 99.76 318.225 97.2 319.425 92.96H328.945C327.185 101.44 320.865 106.8 309.745 106.8ZM309.265 70.96C303.585 70.96 299.985 74.24 299.105 80.8H319.505C318.625 74.24 314.945 70.96 309.265 70.96ZM349.936 106.8C338.816 106.8 331.776 98.88 331.776 85.36C331.776 71.84 338.816 63.92 349.936 63.92C356.176 63.92 360.336 66.48 362.656 70.96V49.04H372.976V106H363.136V98.96C360.896 103.76 356.816 106.8 349.936 106.8ZM352.336 98.96C358.496 98.96 362.656 94.8 362.656 85.36C362.656 75.92 358.496 71.76 352.336 71.76C346.256 71.76 342.096 75.92 342.096 85.36C342.096 94.8 346.256 98.96 352.336 98.96Z" fill="currentColor"/>' +
11
- '</svg>';
12
- /** The page the browser shows when it comes back to the CLI: the sign-in
13
- * screens' look (dark, the lockup, one cut-corner panel; styles/invited.css
14
- * tokens copied here, since this page loads nothing from anywhere). The
15
- * stylesheet carries a per-response nonce, so the policy admits nothing else. */
16
- function page(response, status, title, message, after) {
17
- const nonce = randomBytes(16).toString('base64url');
18
- const tone = status === 200 ? 'Connected' : 'Not connected';
19
- const style = `:root{color-scheme:dark}*{box-sizing:border-box}` +
20
- `body{margin:0;min-height:100dvh;display:grid;place-items:center;padding:32px 24px;background:#121212;color:#f5f5f5;` +
21
- `font:400 14px/20px 'Creato Display','Helvetica Neue',Arial,sans-serif;-webkit-font-smoothing:antialiased}` +
22
- // The design system's cut-corner frame (styles/invited.css, .iv-dialog): a
23
- // 1px frame in the line colour, and the surface inside it cut 2 − √2 of a
24
- // pixel less, so the diagonal frame is as thick as the straight one.
25
- `main{width:100%;max-width:440px;padding:1px;background:#4a4a4a;` +
26
- `clip-path:polygon(0 0,calc(100% - 16px) 0,100% 16px,100% 100%,16px 100%,0 calc(100% - 16px))}` +
27
- `main>div{padding:40px 36px 36px;background:#1c1c1c;` +
28
- `clip-path:polygon(0 0,calc(100% - 15.414px) 0,100% 15.414px,100% 100%,15.414px 100%,0 calc(100% - 15.414px))}` +
29
- `.lockup{display:block;width:clamp(91px,30vw,138px);height:auto;margin:0 0 28px -10px;color:#f5f5f5}` +
30
- `.eyebrow{margin:0 0 12px;font:400 11px/16px 'B612 Mono',ui-monospace,Menlo,Consolas,monospace;letter-spacing:.08em;text-transform:uppercase;color:#bdbdbd}` +
31
- `.eyebrow b{font-weight:400;color:${status === 200 ? '#8fd3a4' : '#f0b36c'}}` +
32
- `h1{margin:0 0 10px;font:500 24px/30px 'Creato Display','Helvetica Neue',Arial,sans-serif;letter-spacing:-.3px;text-wrap:balance}` +
33
- `p{margin:0;color:#bdbdbd}` +
34
- `@media (max-width:480px){main>div{padding:28px 24px 24px}}`;
35
- const body = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">` +
36
- `<title>${escape(title)}</title><style nonce="${nonce}">${style}</style></head>` +
37
- `<body><main><div>${LOCKUP.replace('<svg ', '<svg class="lockup" ')}` +
38
- `<p class="eyebrow">Invited CLI · <b>${tone}</b></p>` +
39
- `<h1>${escape(title)}</h1><p>${escape(message)}</p></div></main></body></html>`;
3
+ /** Where the browser goes once it has come back to the CLI: Invited's own page for it (since 0.2.1; owner, 8 November
4
+ * 2026: "align that styling with the new pages"), the log-in screens' frame with the clouds, which says in a line
5
+ * what the terminal says in full. The address carries only that word, and no referrer goes with it, so Invited never
6
+ * sees the code. */
7
+ function sendOn(response, issuer, result, after) {
8
+ response.writeHead(303, {
9
+ Location: `${issuer}/auth/cli?result=${result}`,
10
+ 'Cache-Control': 'no-store',
11
+ 'Referrer-Policy': 'no-referrer',
12
+ Connection: 'close',
13
+ });
14
+ response.end(after);
15
+ }
16
+ /** A request that is no log-in's answer (another host, method or path): a line of text, and nowhere to go. */
17
+ function refuse(response, status, message) {
40
18
  response.writeHead(status, {
41
- 'Content-Type': 'text/html; charset=utf-8',
19
+ 'Content-Type': 'text/plain; charset=utf-8',
42
20
  'Cache-Control': 'no-store',
43
- 'Content-Security-Policy': `default-src 'none'; style-src 'nonce-${nonce}'`,
44
21
  'X-Content-Type-Options': 'nosniff',
45
- 'Referrer-Policy': 'no-referrer',
46
22
  Connection: 'close',
47
23
  });
48
- response.end(body, after);
24
+ response.end(message);
49
25
  }
50
26
  export async function listenForCallback(options) {
51
27
  let settle;
@@ -56,39 +32,39 @@ export async function listenForCallback(options) {
56
32
  const server = createServer((request, response) => {
57
33
  const host = `127.0.0.1:${port}`;
58
34
  if (finished) {
59
- // The same answer again (a browser retrying its navigation) sees the
60
- // same page; anything else learns the sign-in is over.
35
+ // The same answer again (a browser retrying its navigation) goes where
36
+ // it went; anything else learns the log-in has ended.
61
37
  const again = new URL(request.url ?? '/', `http://${host}`);
62
38
  if (answered && again.pathname === '/callback' && again.searchParams.get('state') === options.state)
63
- return page(response, answered.status, answered.title, answered.message);
64
- return page(response, 410, 'Already finished', 'This sign-in has finished. Return to the terminal.');
39
+ return sendOn(response, options.issuer, answered);
40
+ return sendOn(response, options.issuer, 'ended');
65
41
  }
66
42
  if (request.headers.host !== host)
67
- return page(response, 400, 'Not this sign-in', 'This address is only for the Invited CLI on this computer.');
43
+ return refuse(response, 400, 'This address is only for the Invited CLI on this computer.');
68
44
  if (request.method !== 'GET')
69
- return page(response, 405, 'Not allowed', 'Return to the terminal.');
45
+ return refuse(response, 405, 'Not allowed. Return to the terminal.');
70
46
  const url = new URL(request.url ?? '/', `http://${host}`);
71
47
  if (url.pathname !== '/callback')
72
- return page(response, 404, 'Not found', 'Return to the terminal.');
48
+ return refuse(response, 404, 'Not found. Return to the terminal.');
73
49
  const query = url.searchParams;
74
- // Another request (a stray tab, a page probing ports) changes nothing.
50
+ // Another log-in's answer (a stray tab) changes nothing.
75
51
  if (query.getAll('state').length !== 1 || query.get('state') !== options.state)
76
- return page(response, 400, 'Not this sign-in', 'This answer does not belong to the sign-in the terminal started. Return to the terminal.');
77
- const finish = (status, title, message, result) => {
52
+ return sendOn(response, options.issuer, 'ended');
53
+ const finish = (result, outcome) => {
78
54
  finished = true;
79
- answered = { status, title, message };
80
- page(response, status, title, message, () => settle(result));
55
+ answered = result;
56
+ sendOn(response, options.issuer, result, () => settle(outcome));
81
57
  };
82
58
  const iss = query.get('iss');
83
59
  if ((options.requireIss && iss === null) || (iss !== null && iss !== options.issuer))
84
- return finish(400, 'Refused', 'The answer did not come from Invited. Return to the terminal.', {
60
+ return finish('failed', {
85
61
  ok: false,
86
62
  message: `The browser's answer came from ${iss ?? 'an unnamed server'}, not ${options.issuer}, so it was refused. Run \`invited login\` again.`,
87
63
  });
88
64
  const error = query.get('error');
89
65
  if (error !== null) {
90
66
  const description = query.get('error_description');
91
- return finish(400, 'Not connected', 'The Invited CLI was not connected. Return to the terminal.', {
67
+ return finish(error === 'access_denied' ? 'cancelled' : 'failed', {
92
68
  ok: false,
93
69
  message: error === 'access_denied'
94
70
  ? 'You cancelled the connection in your browser, so nothing was connected.'
@@ -97,14 +73,11 @@ export async function listenForCallback(options) {
97
73
  }
98
74
  const code = query.get('code');
99
75
  if (!code)
100
- return finish(400, 'Not connected', 'The answer had no code. Return to the terminal.', {
76
+ return finish('failed', {
101
77
  ok: false,
102
- message: 'The browser came back without a sign-in code. Run `invited login` again.',
78
+ message: 'The browser came back without a log-in code. Run `invited login` again.',
103
79
  });
104
- return finish(200, 'Invited CLI connected', 'You can close this tab and return to the terminal.', {
105
- ok: true,
106
- code,
107
- });
80
+ return finish('connected', { ok: true, code });
108
81
  });
109
82
  await new Promise((resolve, reject) => {
110
83
  server.once('error', reject);
@@ -122,9 +95,9 @@ export async function listenForCallback(options) {
122
95
  });
123
96
  close();
124
97
  }, options.timeoutMs);
125
- /** Once the browser has been answered, closing lingers a moment: the page
126
- * finishes reaching the browser (it was once cut off mid-response), and a
127
- * browser that retries its navigation gets the same page again instead of
98
+ /** Once the browser has been answered, closing lingers a moment: the answer
99
+ * finishes reaching the browser (a page was once cut off mid-response), and
100
+ * a browser that retries its navigation is sent on again instead of seeing
128
101
  * "This site can't be reached". The time limit closes at once. */
129
102
  function close(force = false) {
130
103
  clearTimeout(timer);
package/dist/main.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { homedir } from 'node:os';
2
2
  import { checkOptions, COMMAND_OPTIONS, parseArgs } from './args.js';
3
3
  import { openBrowser } from './browser.js';
4
- import { fragments, login, logout, privateFragments, settingsLine, status, whoami, } from './commands.js';
4
+ import { assistantsLine, fragments, login, logout, privateFragments, status, whoami, } from './commands.js';
5
5
  import { connect, HOSTS } from './connect.js';
6
6
  import { configDir, credentialStore } from './credentials.js';
7
7
  import { CliError } from './errors.js';
@@ -10,9 +10,11 @@ import { VERSION } from './version.js';
10
10
  /**
11
11
  * `invited`: the command-line client of Invited's MCP connector
12
12
  * (docs/mcp-connector-2026-10-11.md, "3e: CLI"). It configures assistants'
13
- * hosts to use the connector, and signs in itself as the pre-registered
14
- * client `invited-cli` to print the member's public Identity in a terminal.
15
- * It holds an OAuth refresh token like any other client, never a vault key.
13
+ * hosts to use the connector, and logs in itself as the pre-registered
14
+ * client `invited-cli` to print, in a terminal, the fragments the member
15
+ * chooses to share with it on the consent screen (owner, 8 November 2026: the
16
+ * connector reads those alone, never the Page). It holds an OAuth refresh
17
+ * token like any other client, never a vault key.
16
18
  */
17
19
  export const DEFAULT_ORIGIN = 'https://invited.you';
18
20
  const defaults = () => ({
@@ -38,7 +40,7 @@ export function normalOrigin(value) {
38
40
  }
39
41
  const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
40
42
  if (url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback))
41
- throw new CliError(`${value} is not safe for a sign-in: use https, or http only on this computer.`);
43
+ throw new CliError(`${value} is not safe to log in to: use https, or http only on this computer.`);
42
44
  if (url.username || url.password || url.search || url.hash || !['', '/', '/mcp', '/mcp/'].includes(url.pathname))
43
45
  throw new CliError(`Give Invited's address without a path, such as ${DEFAULT_ORIGIN}.`);
44
46
  return url.origin;
@@ -52,54 +54,55 @@ name "invited", keeping every other server in the file as it is. Hosts:
52
54
  cursor ~/.cursor/mcp.json
53
55
  codex ~/.codex/config.toml ($CODEX_HOME)
54
56
  gemini ~/.gemini/settings.json
55
- The host then signs in to Invited itself: you approve it in your browser.
57
+ The host then logs in to Invited itself: you approve it in your browser.
56
58
 
57
59
  --dry-run Show what would be written; write nothing
58
60
  --bridge Use the mcp-remote bridge (npx -y mcp-remote@0.14.3) instead of the
59
- host's own sign-in, for a host version without remote sign-in
61
+ host's own log-in, for a host version without remote log-in
60
62
  --config <file> The configuration file to edit, instead of the host's usual one
61
63
  --origin <url> Invited's address (default ${DEFAULT_ORIGIN})
62
64
  `,
63
- login: `Usage: invited login [--private] [--no-browser] [--origin <url>]
65
+ login: `Usage: invited login [--no-browser] [--origin <url>]
64
66
 
65
67
  Connects this CLI to your Identity: your browser opens Invited, you approve the
66
- Invited CLI, and the browser returns to a listener on this computer
67
- (http://127.0.0.1, a port the system picks), which closes as soon as it answers
68
- or after 5 minutes. The sign-in is saved in ~/.invited/credentials.json.
68
+ Invited CLI and choose the fragments it may read and for how long, and the
69
+ browser returns to a listener on this computer (http://127.0.0.1, a port the
70
+ system picks), which closes as soon as it answers or after 5 minutes. The log-in
71
+ is saved in ~/.invited/credentials.json. (--private, which 0.1.0 needed for this,
72
+ is still taken.)
69
73
 
70
- --private Also ask for your private fragments: in the browser you choose
71
- which to share with this CLI, and for how long
72
74
  --no-browser Print the address to open instead of opening the browser
73
75
  --origin <url> Invited's address (default ${DEFAULT_ORIGIN})
74
76
  `,
75
77
  whoami: `Usage: invited whoami [--json] [--origin <url>]
76
78
 
77
- Prints your public Identity: what your Invited Page publishes for AI readers,
78
- as Markdown (invited://identity), or as JSON with --json (invited://identity.json).
79
+ Prints the fragments you share with this CLI, by section, and when that sharing
80
+ ends: as Markdown (invited://identity), or as JSON with --json
81
+ (invited://identity.json).
79
82
  `,
80
83
  fragments: `Usage: invited fragments --type <type> [--type <type> ...] [--json]
81
84
  invited fragments --search <words> [--json]
82
85
 
83
- Prints your public fragments of the given types (get_fragments), or the ones that
84
- match some words, best first (search_fragments). Types are keys such as skill,
85
- language, experience, cuisines or dietaryPreferences.
86
+ Prints the fragments you share with this CLI of the given types (get_fragments),
87
+ or the ones that match some words, best first (search_fragments). Types are keys
88
+ such as skill, language, experience, cuisines or dietaryPreferences.
86
89
  `,
87
90
  private: `Usage: invited private [--type <type> ...] [--json] [--origin <url>]
88
91
 
89
- Prints the private fragments you share with this CLI (private_fragments), and when
90
- that sharing ends. You choose them in your browser with \`invited login --private\`.
91
- While they are shared, Invited's server can read them to pass them on.
92
+ Prints the fragments you share with this CLI (private_fragments), and when that
93
+ sharing ends. You choose them in your browser with \`invited login\`. While they
94
+ are shared, Invited's server can read them to pass them on.
92
95
  `,
93
96
  status: `Usage: invited status [--json] [--origin <url>]
94
97
 
95
- Shows the sign-in this CLI holds: your Identity and Page, its scopes, when it was
96
- last used and when it expires. It reads the credentials file only. Listing every
97
- assistant you have connected needs your browser: Settings, Connected assistants.
98
+ Shows the log-in this CLI holds: your Identity, its scopes, when it was last
99
+ used and when it expires. It reads the credentials file only. Listing every
100
+ assistant you have connected needs your browser: Activity, Assistants.
98
101
  `,
99
102
  logout: `Usage: invited logout [--origin <url>] (also: invited revoke)
100
103
 
101
- Revokes this CLI's own sign-in at Invited and deletes its credentials. Other
102
- assistants are disconnected in Settings, not here.
104
+ Revokes this CLI's own log-in at Invited and deletes its credentials. Other
105
+ assistants are disconnected in Activity, not here.
103
106
  `,
104
107
  };
105
108
  HELP.revoke = HELP.logout;
@@ -113,28 +116,27 @@ ${style.bold('Usage')}
113
116
 
114
117
  ${style.bold('Commands')}
115
118
  connect <host> Add Invited's MCP server to ${HOSTS.slice(0, -1).join(', ')} or ${HOSTS.at(-1)}
116
- login Connect this CLI to your Identity (you approve in your browser)
117
- whoami Print your public Identity as Markdown
118
- fragments Print public fragments: --type <type> (repeatable) or --search <words>
119
- private Print the private fragments you share with this CLI
120
- status Show the sign-in this CLI holds
121
- logout Revoke this CLI's sign-in and delete its credentials (also: revoke)
119
+ login Connect this CLI to your Identity (you approve and choose in your browser)
120
+ whoami Print the fragments you share with this CLI, as Markdown
121
+ fragments Print shared fragments: --type <type> (repeatable) or --search <words>
122
+ private Print the fragments you share with this CLI and when that ends
123
+ status Show the log-in this CLI holds
124
+ logout Revoke this CLI's log-in and delete its credentials (also: revoke)
122
125
 
123
126
  ${style.bold('Options')}
124
127
  --origin <url> Invited's address (default ${DEFAULT_ORIGIN}, or INVITED_ORIGIN)
125
128
  --json Print JSON (whoami, fragments, private, status)
126
129
  --dry-run connect: show what would be written, write nothing
127
- --bridge connect: use the mcp-remote bridge instead of the host's own sign-in
130
+ --bridge connect: use the mcp-remote bridge instead of the host's own log-in
128
131
  --config <file> connect: the configuration file to edit
129
132
  --no-browser login: print the address instead of opening a browser
130
- --private login: also choose private fragments to share with this CLI
131
133
  -h, --help Show help (invited help <command> for one command)
132
134
  -v, --version Show the version
133
135
 
134
- The CLI keeps its sign-in in ~/.invited/credentials.json (INVITED_CONFIG_DIR moves it)
135
- and reads what your Invited Page publishes, and the private fragments you choose to
136
- share with it, within 60 reads a minute.
137
- ${settingsLine(DEFAULT_ORIGIN)}
136
+ The CLI keeps its log-in in ~/.invited/credentials.json (INVITED_CONFIG_DIR moves it)
137
+ and reads only the fragments you choose to share with it, for as long as you chose,
138
+ within 60 reads a minute.
139
+ ${assistantsLine(DEFAULT_ORIGIN)}
138
140
  `;
139
141
  }
140
142
  /** Runs one command; resolves to the exit code (0 or 1). */
@@ -182,7 +184,7 @@ export async function main(argv, partial = {}) {
182
184
  const ctx = { ...environment, origin, store, style };
183
185
  switch (command) {
184
186
  case 'login':
185
- await login(ctx, { noBrowser: flags.noBrowser, private: flags.private });
187
+ await login(ctx, { noBrowser: flags.noBrowser });
186
188
  return 0;
187
189
  case 'whoami':
188
190
  await whoami(ctx, flags.json);
package/dist/mcp.js CHANGED
@@ -19,7 +19,7 @@ export const headerValue = (value) => /^[\x20-\x7e]*$/.test(value) && !/^=\?base
19
19
  ? value
20
20
  : `=?base64?${Buffer.from(value, 'utf8').toString('base64')}?=`;
21
21
  /** A JSON-RPC error in an answer the server sent with HTTP 200 (an unknown
22
- * resource, a Page with nothing for AI readers). */
22
+ * resource or tool). */
23
23
  export class McpError extends CliError {
24
24
  code;
25
25
  data;
@@ -90,7 +90,7 @@ async function answer(response, id) {
90
90
  return outcome(body);
91
91
  case 401: {
92
92
  const reason = typeof body.error_description === 'string' ? ` (${body.error_description.replace(/\.$/, '')})` : '';
93
- throw new CliError(`Invited did not accept this CLI's sign-in${reason}. Run \`invited login\` to connect again.`);
93
+ throw new CliError(`Invited did not accept this CLI's log-in${reason}. Run \`invited login\` to connect again.`);
94
94
  }
95
95
  case 403: {
96
96
  const challenge = response.headers.get('www-authenticate') ?? '';
@@ -103,7 +103,7 @@ async function answer(response, id) {
103
103
  case 429: {
104
104
  const data = (error.data ?? {});
105
105
  const wait = retryAfter(response) ?? (typeof data.retryAfter === 'number' ? data.retryAfter : 60);
106
- throw new CliError(`Too many reads: this connection has used its 60 reads a minute, which every Invited CLI signed in to your Identity shares. Try again in ${wait} seconds.`);
106
+ throw new CliError(`Too many reads: this connection has used its 60 reads a minute, which every Invited CLI logged in to your Identity shares. Try again in ${wait} seconds.`);
107
107
  }
108
108
  case 503:
109
109
  throw new CliError(`Invited could not complete this request. Try again in ${retryAfter(response) ?? 30} seconds.`);
package/dist/oauth.js CHANGED
@@ -11,9 +11,10 @@ import { readJson, retryAfter, send } from './http.js';
11
11
  * given: discovery must name that origin as the issuer and every endpoint.
12
12
  */
13
13
  export const CLIENT_ID = 'invited-cli';
14
- /** What the CLI asks for: the public Identity, and with `login --private` the
15
- * private fragments the member chooses to share on the consent screen
16
- * (Phase 4, step 4b). */
14
+ /** What the CLI asks for, every log-in: both scopes, as the consent screen
15
+ * grants identity:private only beside identity:public. Since 8 November 2026
16
+ * every read needs identity:private: the fragments the member chooses to share
17
+ * on the consent screen (Phase 4, step 4b), never the Page. */
17
18
  export const PUBLIC_SCOPE = 'identity:public';
18
19
  export const PRIVATE_SCOPE = 'identity:private';
19
20
  export const mcpUrl = (origin) => origin + '/mcp';
@@ -23,7 +24,7 @@ async function getJson(fetch, url) {
23
24
  throw new CliError(`${new URL(url).origin} answered ${response.status} for ${new URL(url).pathname}.`);
24
25
  return readJson(response);
25
26
  }
26
- const notInvited = (origin) => new CliError(`${origin} does not describe itself as Invited's connector, so this CLI will not sign in there.`);
27
+ const notInvited = (origin) => new CliError(`${origin} does not describe itself as Invited's connector, so this CLI will not log in there.`);
27
28
  /** The protected resource metadata (RFC 9728), then the authorization server
28
29
  * metadata (RFC 8414), each checked against the origin. */
29
30
  export async function discover(fetch, origin) {
@@ -105,7 +106,7 @@ async function tokenRequest(fetch, endpoint, form, now) {
105
106
  });
106
107
  const body = await readJson(response);
107
108
  if (response.status === 429)
108
- throw new CliError(`Invited is limiting requests for this sign-in. Try again in ${retryAfter(response) ?? 60} seconds.`);
109
+ throw new CliError(`Invited is limiting requests for this log-in. Try again in ${retryAfter(response) ?? 60} seconds.`);
109
110
  if (response.status >= 500)
110
111
  throw new CliError(`Invited could not complete this request. Try again in ${retryAfter(response) ?? 30} seconds.`);
111
112
  if (response.status !== 200)
@@ -161,6 +162,5 @@ export async function revokeRefreshToken(fetch, connection) {
161
162
  const body = await readJson(response);
162
163
  const wait = retryAfter(response);
163
164
  const detail = typeof body.error_description === 'string' ? `: ${body.error_description}` : '';
164
- throw new CliError(`Invited did not revoke the sign-in (${response.status}${detail}).` +
165
- (wait ? ` Try again in ${wait} seconds.` : ''));
165
+ throw new CliError(`Invited did not revoke the log-in (${response.status}${detail}).` + (wait ? ` Try again in ${wait} seconds.` : ''));
166
166
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invitedlabs/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "The invited command: connect terminal agents and AI assistants to your Invited Identity through Invited's MCP connector.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://invited.you",