@invitedlabs/cli 0.1.0 → 0.2.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 +35 -24
- package/dist/commands.js +129 -162
- package/dist/connect.js +4 -4
- package/dist/loopback.js +11 -11
- package/dist/main.js +41 -39
- package/dist/mcp.js +3 -3
- package/dist/oauth.js +7 -7
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
13
|
-
npx @invitedlabs/cli login # or
|
|
14
|
-
npx @invitedlabs/cli whoami # then print
|
|
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
|
|
28
|
-
| `invited login` |
|
|
29
|
-
| `invited whoami` | Prints
|
|
30
|
-
| `invited fragments --type <type>` | Prints
|
|
31
|
-
| `invited fragments --search <words>` | Prints the
|
|
32
|
-
| `invited private` | Prints the
|
|
33
|
-
| `invited status` | Shows the
|
|
34
|
-
| `invited logout` | Revokes this CLI's own
|
|
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
|
-
|
|
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
|
-
|
|
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,34 @@ 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
|
|
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
|
|
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
|
|
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
|
|
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 `
|
|
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
|
-
##
|
|
62
|
+
## What it reads
|
|
61
63
|
|
|
62
|
-
|
|
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
|
|
67
|
-
2. To remove the connection itself, use
|
|
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.0
|
|
74
|
+
|
|
75
|
+
- 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.
|
|
76
|
+
|
|
77
|
+
- 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).
|
|
78
|
+
- `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.
|
|
79
|
+
- Your access log keeps each read for 3 months.
|
|
80
|
+
- "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
|
-
|
|
8
|
-
|
|
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
|
|
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
|
-
/**
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
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
|
-
/**
|
|
68
|
-
|
|
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
|
|
83
|
-
|
|
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
|
|
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
|
|
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
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
await
|
|
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
|
-
(
|
|
149
|
-
|
|
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
|
-
|
|
159
|
-
let read;
|
|
200
|
+
let text;
|
|
160
201
|
try {
|
|
161
|
-
|
|
202
|
+
text = await readResource(ctx, accessTokens(ctx), json ? 'invited://identity.json' : 'invited://identity');
|
|
162
203
|
}
|
|
163
204
|
catch (error) {
|
|
164
|
-
|
|
165
|
-
throw new CliError(error.message);
|
|
166
|
-
throw error;
|
|
205
|
+
refused(error);
|
|
167
206
|
}
|
|
168
|
-
ctx.out(
|
|
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
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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 =
|
|
214
|
-
? `of type ${
|
|
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
|
|
230
|
+
ctx.out(`No shared fragments ${what}.\n`);
|
|
218
231
|
return;
|
|
219
232
|
}
|
|
220
|
-
const lines = [`${plural(list.length, '
|
|
221
|
-
for (const fragment of list)
|
|
222
|
-
lines.push(
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
* or the
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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, '
|
|
255
|
+
const lines = [`${plural(list.length, 'fragment')}${what} shared with the Invited CLI ${until}.`, ''];
|
|
281
256
|
for (const fragment of list)
|
|
282
|
-
lines.push(
|
|
283
|
-
lines.push('',
|
|
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 (
|
|
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
|
-
|
|
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 ===
|
|
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
|
|
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
|
-
|
|
352
|
-
? 'Run `invited
|
|
353
|
-
: 'It reads no
|
|
354
|
-
'The CLI can end only its own
|
|
355
|
-
|
|
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
|
|
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
|
|
375
|
-
: `The Invited CLI was not
|
|
376
|
-
`Other assistants stay connected. ${
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
@@ -2,12 +2,12 @@ import { randomBytes } from 'node:crypto';
|
|
|
2
2
|
import { createServer } from 'node:http';
|
|
3
3
|
import { duration } from './format.js';
|
|
4
4
|
const escape = (text) => text.replace(/[&<>"']/g, character => `&#${character.charCodeAt(0)};`);
|
|
5
|
-
/** The Invited lockup (public/brand/
|
|
6
|
-
*
|
|
7
|
-
const LOCKUP = '<svg viewBox="
|
|
8
|
-
'<path d="
|
|
9
|
-
'<path d="
|
|
10
|
-
'<path d="
|
|
5
|
+
/** The Invited lockup, the Missing piece and the wordmark (public/brand/missing-piece/lockup-ink.svg, the Figma
|
|
6
|
+
* kit's drawing), drawn in the page's ink. */
|
|
7
|
+
const LOCKUP = '<svg viewBox="69 90.25 394.75 105.75" fill="none" xmlns="http://www.w3.org/2000/svg" aria-label="Invited" role="img">' +
|
|
8
|
+
'<path fill-rule="evenodd" clip-rule="evenodd" d="M70.2305 150.001C70.2305 161.904 74.9589 173.319 83.3755 181.736C91.7921 190.153 103.208 194.881 115.11 194.881C127.013 194.881 138.429 190.153 146.845 181.736C155.262 173.319 159.99 161.904 159.99 150.001C159.99 138.098 155.262 126.683 146.845 118.266C138.429 109.85 127.013 105.121 115.11 105.121C103.208 105.121 91.7921 109.85 83.3755 118.266C74.9589 126.683 70.2305 138.098 70.2305 150.001ZM118.898 129.349C118.898 125.521 119.959 124.453 123.794 124.453H135.762C139.59 124.453 140.658 125.521 140.658 129.349V141.317C140.658 145.153 139.59 146.213 135.762 146.213H123.794C119.959 146.213 118.898 145.153 118.898 141.317V129.349Z" fill="currentColor"/>' +
|
|
9
|
+
'<path d="M149.268 100.665C148.336 96.9456 149.111 95.6604 152.831 94.7288L164.445 91.832C168.165 90.9072 169.45 91.6824 170.382 95.402L173.278 107.016C174.203 110.736 173.428 112.021 169.708 112.946L158.094 115.843C154.374 116.774 153.089 115.999 152.164 112.28L149.268 100.665Z" fill="currentColor"/>' +
|
|
10
|
+
'<path d="M211.085 184.179H198.029V115.827H211.085V184.179ZM261.975 184.179H249.591V153.843C249.591 147.123 247.191 143.763 241.143 143.763C234.615 143.763 230.391 148.467 230.391 156.531V184.179H218.007V134.643H229.815V142.707C232.503 137.427 236.919 133.683 244.983 133.683C256.311 133.683 261.975 139.443 261.975 151.443V184.179ZM295.223 184.179H280.247L261.911 134.643H274.871L287.735 173.331L300.599 134.643H313.559L295.223 184.179ZM328.225 184.179H315.841V134.643H328.225V184.179ZM322.081 127.251C317.473 127.251 314.881 124.467 314.881 120.819C314.881 117.171 317.473 114.387 322.081 114.387C326.689 114.387 329.185 117.171 329.185 120.819C329.185 124.467 326.689 127.251 322.081 127.251ZM353.535 184.659C342.303 184.659 338.559 181.203 338.559 170.259V143.475H330.783V134.643H338.559V119.667H350.943V134.643H361.695V143.475H350.943V170.739C350.943 174.579 352.575 175.923 356.511 175.923L361.695 175.827V184.179C359.679 184.371 355.743 184.659 353.535 184.659ZM386.843 185.139C370.715 185.139 361.691 175.059 361.691 159.411C361.691 144.147 370.331 133.683 386.267 133.683C402.299 133.683 410.939 144.243 410.939 158.835C410.939 159.891 410.843 160.851 410.747 162.003H374.075C374.459 172.083 379.259 176.691 386.843 176.691C392.987 176.691 397.019 173.619 398.459 168.531H409.883C407.771 178.707 400.187 185.139 386.843 185.139ZM386.267 142.131C379.451 142.131 375.131 146.067 374.075 153.939H398.555C397.499 146.067 393.083 142.131 386.267 142.131ZM435.073 185.139C421.729 185.139 413.281 175.635 413.281 159.411C413.281 143.187 421.729 133.683 435.073 133.683C442.561 133.683 447.553 136.755 450.337 142.131V115.827H462.721V184.179H450.912V175.731C448.225 181.491 443.329 185.139 435.073 185.139ZM437.953 175.731C445.345 175.731 450.337 170.739 450.337 159.411C450.337 148.083 445.345 143.091 437.953 143.091C430.657 143.091 425.665 148.083 425.665 159.411C425.665 170.739 430.657 175.731 437.953 175.731Z" fill="currentColor"/>' +
|
|
11
11
|
'</svg>';
|
|
12
12
|
/** The page the browser shows when it comes back to the CLI: the sign-in
|
|
13
13
|
* screens' look (dark, the lockup, one cut-corner panel; styles/invited.css
|
|
@@ -26,7 +26,7 @@ function page(response, status, title, message, after) {
|
|
|
26
26
|
`clip-path:polygon(0 0,calc(100% - 16px) 0,100% 16px,100% 100%,16px 100%,0 calc(100% - 16px))}` +
|
|
27
27
|
`main>div{padding:40px 36px 36px;background:#1c1c1c;` +
|
|
28
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(
|
|
29
|
+
`.lockup{display:block;width:clamp(70px,23vw,107px);height:auto;margin:0 0 28px;color:#f5f5f5}` +
|
|
30
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
31
|
`.eyebrow b{font-weight:400;color:${status === 200 ? '#8fd3a4' : '#f0b36c'}}` +
|
|
32
32
|
`h1{margin:0 0 10px;font:500 24px/30px 'Creato Display','Helvetica Neue',Arial,sans-serif;letter-spacing:-.3px;text-wrap:balance}` +
|
|
@@ -61,10 +61,10 @@ export async function listenForCallback(options) {
|
|
|
61
61
|
const again = new URL(request.url ?? '/', `http://${host}`);
|
|
62
62
|
if (answered && again.pathname === '/callback' && again.searchParams.get('state') === options.state)
|
|
63
63
|
return page(response, answered.status, answered.title, answered.message);
|
|
64
|
-
return page(response, 410, 'Already finished', 'This
|
|
64
|
+
return page(response, 410, 'Already finished', 'This log-in has finished. Return to the terminal.');
|
|
65
65
|
}
|
|
66
66
|
if (request.headers.host !== host)
|
|
67
|
-
return page(response, 400, 'Not this
|
|
67
|
+
return page(response, 400, 'Not this log-in', 'This address is only for the Invited CLI on this computer.');
|
|
68
68
|
if (request.method !== 'GET')
|
|
69
69
|
return page(response, 405, 'Not allowed', 'Return to the terminal.');
|
|
70
70
|
const url = new URL(request.url ?? '/', `http://${host}`);
|
|
@@ -73,7 +73,7 @@ export async function listenForCallback(options) {
|
|
|
73
73
|
const query = url.searchParams;
|
|
74
74
|
// Another request (a stray tab, a page probing ports) changes nothing.
|
|
75
75
|
if (query.getAll('state').length !== 1 || query.get('state') !== options.state)
|
|
76
|
-
return page(response, 400, 'Not this
|
|
76
|
+
return page(response, 400, 'Not this log-in', 'This answer does not belong to the log-in the terminal started. Return to the terminal.');
|
|
77
77
|
const finish = (status, title, message, result) => {
|
|
78
78
|
finished = true;
|
|
79
79
|
answered = { status, title, message };
|
|
@@ -99,7 +99,7 @@ export async function listenForCallback(options) {
|
|
|
99
99
|
if (!code)
|
|
100
100
|
return finish(400, 'Not connected', 'The answer had no code. Return to the terminal.', {
|
|
101
101
|
ok: false,
|
|
102
|
-
message: 'The browser came back without a
|
|
102
|
+
message: 'The browser came back without a log-in code. Run `invited login` again.',
|
|
103
103
|
});
|
|
104
104
|
return finish(200, 'Invited CLI connected', 'You can close this tab and return to the terminal.', {
|
|
105
105
|
ok: true,
|
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,
|
|
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
|
|
14
|
-
* client `invited-cli` to print
|
|
15
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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 [--
|
|
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
|
|
67
|
-
(http://127.0.0.1, a port the
|
|
68
|
-
or after 5 minutes. The
|
|
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
|
|
78
|
-
as Markdown (invited://identity), or as JSON with --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
|
|
84
|
-
match some words, best first (search_fragments). Types are keys
|
|
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
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
96
|
-
|
|
97
|
-
assistant you have connected needs your browser:
|
|
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
|
|
102
|
-
assistants are disconnected in
|
|
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
|
|
118
|
-
fragments Print
|
|
119
|
-
private Print the
|
|
120
|
-
status Show the
|
|
121
|
-
logout Revoke this CLI's
|
|
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
|
|
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
|
|
135
|
-
and reads
|
|
136
|
-
|
|
137
|
-
${
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
15
|
-
* private
|
|
16
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|