implto-tos-axi 0.1.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/LICENSE +21 -0
- package/README.md +255 -0
- package/dist/bin/implto-tos-axi.d.ts +2 -0
- package/dist/bin/implto-tos-axi.js +16 -0
- package/dist/bin/implto-tos-axi.js.map +1 -0
- package/dist/src/auth/loopback.d.ts +29 -0
- package/dist/src/auth/loopback.js +110 -0
- package/dist/src/auth/loopback.js.map +1 -0
- package/dist/src/auth/open-browser.d.ts +9 -0
- package/dist/src/auth/open-browser.js +24 -0
- package/dist/src/auth/open-browser.js.map +1 -0
- package/dist/src/auth/provider.d.ts +39 -0
- package/dist/src/auth/provider.js +106 -0
- package/dist/src/auth/provider.js.map +1 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +183 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/commands/components.d.ts +40 -0
- package/dist/src/commands/components.js +417 -0
- package/dist/src/commands/components.js.map +1 -0
- package/dist/src/commands/context.d.ts +10 -0
- package/dist/src/commands/context.js +46 -0
- package/dist/src/commands/context.js.map +1 -0
- package/dist/src/commands/guide.d.ts +16 -0
- package/dist/src/commands/guide.js +203 -0
- package/dist/src/commands/guide.js.map +1 -0
- package/dist/src/commands/home.d.ts +9 -0
- package/dist/src/commands/home.js +33 -0
- package/dist/src/commands/home.js.map +1 -0
- package/dist/src/commands/init.d.ts +12 -0
- package/dist/src/commands/init.js +65 -0
- package/dist/src/commands/init.js.map +1 -0
- package/dist/src/commands/login.d.ts +13 -0
- package/dist/src/commands/login.js +137 -0
- package/dist/src/commands/login.js.map +1 -0
- package/dist/src/commands/tasks.d.ts +8 -0
- package/dist/src/commands/tasks.js +344 -0
- package/dist/src/commands/tasks.js.map +1 -0
- package/dist/src/commands/tools.d.ts +66 -0
- package/dist/src/commands/tools.js +401 -0
- package/dist/src/commands/tools.js.map +1 -0
- package/dist/src/commands/whoami.d.ts +5 -0
- package/dist/src/commands/whoami.js +66 -0
- package/dist/src/commands/whoami.js.map +1 -0
- package/dist/src/config/binding.d.ts +29 -0
- package/dist/src/config/binding.js +42 -0
- package/dist/src/config/binding.js.map +1 -0
- package/dist/src/config/credentials.d.ts +39 -0
- package/dist/src/config/credentials.js +62 -0
- package/dist/src/config/credentials.js.map +1 -0
- package/dist/src/config/profiles.d.ts +24 -0
- package/dist/src/config/profiles.js +64 -0
- package/dist/src/config/profiles.js.map +1 -0
- package/dist/src/errors.d.ts +21 -0
- package/dist/src/errors.js +40 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/flags.d.ts +50 -0
- package/dist/src/flags.js +143 -0
- package/dist/src/flags.js.map +1 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.js +7 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/mcp/client.d.ts +85 -0
- package/dist/src/mcp/client.js +130 -0
- package/dist/src/mcp/client.js.map +1 -0
- package/dist/src/output.d.ts +8 -0
- package/dist/src/output.js +2 -0
- package/dist/src/output.js.map +1 -0
- package/dist/src/resolve.d.ts +16 -0
- package/dist/src/resolve.js +48 -0
- package/dist/src/resolve.js.map +1 -0
- package/dist/src/scopes.d.ts +9 -0
- package/dist/src/scopes.js +16 -0
- package/dist/src/scopes.js.map +1 -0
- package/dist/src/session.d.ts +40 -0
- package/dist/src/session.js +75 -0
- package/dist/src/session.js.map +1 -0
- package/dist/src/skill.d.ts +32 -0
- package/dist/src/skill.js +149 -0
- package/dist/src/skill.js.map +1 -0
- package/dist/src/version.d.ts +5 -0
- package/dist/src/version.js +6 -0
- package/dist/src/version.js.map +1 -0
- package/package.json +70 -0
- package/skills/implto-tos-delivery/SKILL.md +45 -0
- package/skills/implto-tos-tasks/SKILL.md +48 -0
- package/skills/implto-tos-workspace/SKILL.md +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Valentyn Morenko
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# implto-tos-axi
|
|
2
|
+
|
|
3
|
+
Work with [Transformation OS](https://github.com/implementedtoday/Digital-Transformation-OS) from
|
|
4
|
+
the repository you are changing: find the task a change belongs to, keep the deliverables tree
|
|
5
|
+
honest, and reach every other tool the platform has - without carrying fifty tool schemas in
|
|
6
|
+
every model request. Built to [AXI](https://github.com/kunchenguid/axi) conventions, so an agent
|
|
7
|
+
can drive it as comfortably as a person can.
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/implto-tos-axi)
|
|
10
|
+
|
|
11
|
+
## Why this exists
|
|
12
|
+
|
|
13
|
+
Transformation OS already speaks MCP, and an agent can mount the server directly. The cost is
|
|
14
|
+
that all 50 tool schemas travel in the prompt of every request the agent makes, whether or not
|
|
15
|
+
the task has anything to do with a ticket.
|
|
16
|
+
|
|
17
|
+
This CLI is the same server reached a different way. The schemas stay on the server; a ~2 KB
|
|
18
|
+
skill stub says when to reach for the tool; and the one tool a task turns out to need is fetched
|
|
19
|
+
on the spot with `tools show`. An agent that never touches a ticket pays for the stub and
|
|
20
|
+
nothing else.
|
|
21
|
+
|
|
22
|
+
The trade is real but small: a command is two round trips instead of one, because the server is
|
|
23
|
+
stateless and every invocation does its own `initialize`.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
npm install -g implto-tos-axi
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or run it without installing anything:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
npx -y implto-tos-axi whoami
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
To let an agent drive it, add the skills. They are separate on purpose - a session that files a
|
|
38
|
+
ticket should not also load the guidance for process maps:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
npx skills add implementedtoday/implto-tos-axi --skill implto-tos-tasks -g
|
|
42
|
+
npx skills add implementedtoday/implto-tos-axi --skill implto-tos-delivery -g
|
|
43
|
+
npx skills add implementedtoday/implto-tos-axi --skill implto-tos-workspace -g
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
| Skill | Loaded when the task is about |
|
|
47
|
+
| ---------------------- | ---------------------------------------------------------------------- |
|
|
48
|
+
| `implto-tos-tasks` | tracking work: matching a change to a task, filing one, closing it out |
|
|
49
|
+
| `implto-tos-delivery` | what is being built: the component tree, estimates, delivery phases |
|
|
50
|
+
| `implto-tos-workspace` | everything else: process maps, people, roles, systems, client threads |
|
|
51
|
+
|
|
52
|
+
## Sign in
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
implto-tos-axi login # opens a browser
|
|
56
|
+
implto-tos-axi login --profile dev
|
|
57
|
+
implto-tos-axi login --scopes tasks # ask for one area only
|
|
58
|
+
implto-tos-axi login --token tos_… # headless: paste a personal key
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The browser flow is OAuth 2.1 against the deployment itself: the CLI registers itself, you
|
|
62
|
+
approve it on a page that says what it is asking for, and the credential it gets acts as you -
|
|
63
|
+
your workspaces, your name on every write. It refreshes itself for an hour at a time without
|
|
64
|
+
anybody present.
|
|
65
|
+
|
|
66
|
+
`--scopes` narrows the grant to some of the server's areas, and the narrowing is real: a
|
|
67
|
+
credential minted for `tasks` is refused the rest, and `tools list` serves it only that area.
|
|
68
|
+
|
|
69
|
+
With no terminal attached the browser path refuses (`NEEDS_HUMAN`) rather than opening a window
|
|
70
|
+
nobody will see. For CI or a container, mint a key at **Settings → Personal credentials** and set `TOS_TOKEN`; it
|
|
71
|
+
outranks anything on disk. Add `TOS_URL` when the deployment is not prod.
|
|
72
|
+
|
|
73
|
+
## Bind a repository
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
implto-tos-axi init --workspace "Implemented Today" --project DT
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Writes `.implto-tos.json`, which every command finds by walking up from the working directory.
|
|
80
|
+
It is what lets the rest of the commands take names instead of ids, and what fills in
|
|
81
|
+
`workspaceId` on a raw tool call. Run `init` bare to list what you can reach.
|
|
82
|
+
|
|
83
|
+
## What the tool does
|
|
84
|
+
|
|
85
|
+
### Matches a change to the task it belongs to
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
implto-tos-axi context # run this first
|
|
89
|
+
implto-tos-axi tasks search "login redirect" --open
|
|
90
|
+
implto-tos-axi tasks show TO-112
|
|
91
|
+
implto-tos-axi tasks create "Fix the import guard" --assign me
|
|
92
|
+
implto-tos-axi tasks update TO-112 --status "In Progress"
|
|
93
|
+
implto-tos-axi tasks comment TO-112 "Shipped in #204"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`context` is the one call worth making at the start of a session: it answers which project this
|
|
97
|
+
directory is, what the statuses are called, and which of an issue's two assignee fields you
|
|
98
|
+
belong in. Everything after it takes names.
|
|
99
|
+
|
|
100
|
+
`implto-tos-axi guide workflow` has the whole match-or-create procedure, including the part
|
|
101
|
+
where you ask the person rather than deciding alone.
|
|
102
|
+
|
|
103
|
+
### Keeps the deliverables tree honest
|
|
104
|
+
|
|
105
|
+
Components are what a workspace is delivering - services, modules, APIs - in a parent-child
|
|
106
|
+
tree, and tasks hang off them. A component that does not exist yet is a task with nowhere to go.
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
implto-tos-axi components list --tree
|
|
110
|
+
implto-tos-axi components show "Billing API"
|
|
111
|
+
implto-tos-axi components create "Billing API" --parent Platform --type API --estimate 40
|
|
112
|
+
implto-tos-axi components update "Billing API" --parent "Payments"
|
|
113
|
+
implto-tos-axi components update "Billing API" --parent none # back to the top level
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`--tree` is the view worth having before creating anything: a flat list with a parent column
|
|
117
|
+
answers filtering questions, and the tree answers the question somebody actually has, which is
|
|
118
|
+
where the new thing belongs.
|
|
119
|
+
|
|
120
|
+
Omitting a link flag leaves it alone; `none` clears it. A component created inside a bound
|
|
121
|
+
checkout is linked to that checkout's project unless you say `--project none`.
|
|
122
|
+
|
|
123
|
+
### Reaches every other tool, without knowing about it
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
implto-tos-axi tools # the areas, and which you hold
|
|
127
|
+
implto-tos-axi tools domains organization # what one covers, and its rules
|
|
128
|
+
implto-tos-axi tools list --domain organization
|
|
129
|
+
implto-tos-axi tools show create_person
|
|
130
|
+
implto-tos-axi tools call create_person name="Ann Lee" email=ann@acme.com
|
|
131
|
+
implto-tos-axi tools call update_process_nodes --json-file nodes.json
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Nothing here is hard-coded: the areas, the tool names and their arguments all come from the
|
|
135
|
+
server, so a tool added on a Tuesday is callable on the Tuesday with no release of this package.
|
|
136
|
+
|
|
137
|
+
Arguments are `name=value`, typed against the tool's own schema - `estimateSecs=3600` arrives as
|
|
138
|
+
a number, and a nullable field is cleared with `field=null`. `key=@file` reads a value from a
|
|
139
|
+
file (which is how a markdown body or a mermaid diagram survives a shell), `key=@-` reads stdin,
|
|
140
|
+
and `--json` passes a nested argument whole.
|
|
141
|
+
|
|
142
|
+
A misspelled argument is refused here, naming the real ones. That matters more than it sounds:
|
|
143
|
+
the server ignores an argument it does not know, so the call would otherwise succeed without
|
|
144
|
+
doing what was asked.
|
|
145
|
+
|
|
146
|
+
### Takes names, not ids
|
|
147
|
+
|
|
148
|
+
Every workspace, project, status, component, parent and system is matched by name,
|
|
149
|
+
case-insensitively, against what the server just returned. Two matches is an error naming both
|
|
150
|
+
rather than a guess, and the error carries the full list of what was available.
|
|
151
|
+
|
|
152
|
+
The one id you never type is `workspaceId`: it comes from the binding, or from
|
|
153
|
+
`--workspace <name>` anywhere else, and a raw `tools call` reports which one it used.
|
|
154
|
+
|
|
155
|
+
### Says what to run next
|
|
156
|
+
|
|
157
|
+
Every payload ends with a `help` list, and so does every error - same field, same shape - so
|
|
158
|
+
recovering from a failure reads like reading a result:
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
error: This directory is not bound to a Transformation OS project.
|
|
162
|
+
code: NO_BINDING
|
|
163
|
+
help[2]:
|
|
164
|
+
implto-tos-axi init --workspace <name> --project <key>
|
|
165
|
+
implto-tos-axi init # run it bare to list what you can reach
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Configuration
|
|
169
|
+
|
|
170
|
+
| Thing | Where |
|
|
171
|
+
| ----------------------- | -------------------------------------------------------------------------- |
|
|
172
|
+
| Credential | `~/.config/implto-tos-axi/credentials.json`, mode 0600 in a 0700 directory |
|
|
173
|
+
| | `$XDG_CONFIG_HOME` is honoured; `%APPDATA%` on Windows |
|
|
174
|
+
| Repository binding | `.implto-tos.json`, found by walking up from the working directory |
|
|
175
|
+
| `TOS_TOKEN` | A personal key. Outranks the file, and is never written back to it |
|
|
176
|
+
| `TOS_AXI_ALLOW_BROWSER` | Open a browser even with no TTY attached. For a wrapper that knows better |
|
|
177
|
+
|
|
178
|
+
Profiles are deployments, one origin each:
|
|
179
|
+
|
|
180
|
+
| Profile | Origin |
|
|
181
|
+
| ------- | --------------------------------------------- |
|
|
182
|
+
| `prod` | https://implto-transformation-os.web.app |
|
|
183
|
+
| `dev` | https://digital-transformation-os-dev.web.app |
|
|
184
|
+
| `local` | http://localhost:4000 |
|
|
185
|
+
|
|
186
|
+
`--url` points at a deployment by address instead, and `--mcp-url` at the MCP endpoint directly -
|
|
187
|
+
which is what a local MCP server running as its own process on port 3100 needs. Both have an
|
|
188
|
+
environment variable for the headless case, and a flag always beats one.
|
|
189
|
+
|
|
190
|
+
An address is named after the deployment it turns out to be, so a credential taken against dev is
|
|
191
|
+
filed under `dev` whichever way you got there.
|
|
192
|
+
|
|
193
|
+
The origin is not a convenience: it is the OAuth issuer and the resource a token is minted for,
|
|
194
|
+
so a client that talks to one address and authorizes against another ends up with a credential
|
|
195
|
+
for somebody else's server.
|
|
196
|
+
|
|
197
|
+
## Command reference
|
|
198
|
+
|
|
199
|
+
| Command | What it does |
|
|
200
|
+
| ---------------------------------- | ---------------------------------------------------------------------- |
|
|
201
|
+
| `login` | Sign in through the browser, or `--token` to paste a key |
|
|
202
|
+
| `logout` | Forget this machine's credential (revoking it is a button in Settings) |
|
|
203
|
+
| `whoami` | Who this credential acts as, what it reaches, and where it came from |
|
|
204
|
+
| `init` | Bind this directory to a workspace and project |
|
|
205
|
+
| `context` | The project, its statuses and types, and who you are |
|
|
206
|
+
| `tasks search <words>` | Find tasks. `--open`, `--mine`, `--status`, `--all-projects` |
|
|
207
|
+
| `tasks show <key>` | One task in full |
|
|
208
|
+
| `tasks mine` | Your tasks |
|
|
209
|
+
| `tasks create <title>` | File one. `--assign me` by default |
|
|
210
|
+
| `tasks update <key>` | Status, assignee, title, description, priority, due date |
|
|
211
|
+
| `tasks comment <key> <text>` | Leave a note on it |
|
|
212
|
+
| `components list` | The deliverables. `--tree`, `--parent`, `--project`, `--search` |
|
|
213
|
+
| `components show <name>` | One in full, with its phase estimates |
|
|
214
|
+
| `components create <name>` | Add one. `--parent`, `--type`, `--estimate`, `--system`, `--project` |
|
|
215
|
+
| `components update <name>` | Rename, re-parent, re-link, re-estimate |
|
|
216
|
+
| `tools` | The areas the server offers, and which this credential holds |
|
|
217
|
+
| `tools domains <id>` | What one area covers, and the rules for using it |
|
|
218
|
+
| `tools list` | The tools in an area. `--domain`, `--search` |
|
|
219
|
+
| `tools show <name>` | One tool's arguments |
|
|
220
|
+
| `tools call <name> [name=value …]` | Call it |
|
|
221
|
+
| `guide [topic]` | Topic-sized guidance. Run bare to list topics |
|
|
222
|
+
|
|
223
|
+
`--help` on any of them is the current truth; this table is not.
|
|
224
|
+
|
|
225
|
+
Guide topics: `workflow`, `components`, `tools`, `auth`, `binding`, `statuses`, `output`.
|
|
226
|
+
|
|
227
|
+
## Output and exit codes
|
|
228
|
+
|
|
229
|
+
Every command prints [TOON](https://github.com/toon-format/toon): compact, tabular where the
|
|
230
|
+
data is tabular, and readable without a parser. The two whose content is prose rather than data -
|
|
231
|
+
`guide <topic>` and `components list --tree` - print plain text, because a procedure rendered as
|
|
232
|
+
one escaped string is the format fighting the content.
|
|
233
|
+
|
|
234
|
+
`0` succeeded. `2` is the caller's mistake - a bad flag, a name that matches nothing or matches
|
|
235
|
+
several. Everything else is `1`. Every error carries a code (`AUTH_MISSING`, `AUTH_INVALID`,
|
|
236
|
+
`NO_BINDING`, `AMBIGUOUS`, `NOT_FOUND`, `FORBIDDEN`, `NEEDS_HUMAN`, `NETWORK_ERROR`,
|
|
237
|
+
`VALIDATION_ERROR`) and the command that fixes it.
|
|
238
|
+
|
|
239
|
+
## Development
|
|
240
|
+
|
|
241
|
+
```sh
|
|
242
|
+
pnpm install
|
|
243
|
+
pnpm build # tsc, and chmod the bin
|
|
244
|
+
pnpm test
|
|
245
|
+
pnpm typecheck
|
|
246
|
+
pnpm build:skill # regenerate the three SKILL.md stubs from src/skill.ts
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The stubs are generated, and CI fails if the files on disk have drifted from the generator or if
|
|
250
|
+
any of them grows past 2500 characters. That cap is the point of the package: a stub that grows
|
|
251
|
+
into a manual gives back exactly what reaching the server this way bought.
|
|
252
|
+
|
|
253
|
+
## License
|
|
254
|
+
|
|
255
|
+
MIT
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { tryFastPath } from "axi-sdk-js/fast-path";
|
|
3
|
+
import { VERSION } from "../src/version.js";
|
|
4
|
+
// `implto-tos-axi --help | head` closes the pipe early, and an unhandled EPIPE
|
|
5
|
+
// turns that into a stack trace. Piping output somewhere that stops reading is
|
|
6
|
+
// ordinary shell use, not an error worth reporting.
|
|
7
|
+
process.stdout.on("error", (error) => {
|
|
8
|
+
if (error.code === "EPIPE")
|
|
9
|
+
process.exit(0);
|
|
10
|
+
throw error;
|
|
11
|
+
});
|
|
12
|
+
if (!tryFastPath(process.argv.slice(2), { version: VERSION })) {
|
|
13
|
+
const { main } = await import("../src/cli.js");
|
|
14
|
+
await main();
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=implto-tos-axi.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"implto-tos-axi.js","sourceRoot":"","sources":["../../bin/implto-tos-axi.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAE5C,+EAA+E;AAC/E,+EAA+E;AAC/E,oDAAoD;AACpD,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAA4B,EAAE,EAAE;IAC1D,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO;QAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC5C,MAAM,KAAK,CAAC;AACd,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;IAC9D,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,eAAe,CAAC,CAAC;IAC/C,MAAM,IAAI,EAAE,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one-shot listener that catches the authorization code.
|
|
3
|
+
*
|
|
4
|
+
* A desktop client has nowhere else to receive one, which is why RFC 8252 makes
|
|
5
|
+
* loopback the redirect for native applications and why the server matches a
|
|
6
|
+
* loopback address with the port ignored: the operating system picks the port
|
|
7
|
+
* at start-up, so it cannot be registered in advance.
|
|
8
|
+
*
|
|
9
|
+
* Bound to 127.0.0.1 only. `state` is compared in constant time, `iss` is
|
|
10
|
+
* compared against the deployment that was asked, and the server closes after
|
|
11
|
+
* the first answer - so another process on this machine that guessed the port
|
|
12
|
+
* still has to have the `state`, which only the browser was given.
|
|
13
|
+
*/
|
|
14
|
+
export interface LoopbackResult {
|
|
15
|
+
code: string;
|
|
16
|
+
state: string;
|
|
17
|
+
}
|
|
18
|
+
export interface Loopback {
|
|
19
|
+
/** The address to register and to send the browser back to. */
|
|
20
|
+
redirectUri: string;
|
|
21
|
+
state: string;
|
|
22
|
+
/** Resolves with the code, or rejects with LOGIN_TIMEOUT. */
|
|
23
|
+
wait: Promise<LoopbackResult>;
|
|
24
|
+
close(): void;
|
|
25
|
+
}
|
|
26
|
+
export declare function startLoopback(options: {
|
|
27
|
+
issuer: string;
|
|
28
|
+
timeoutMs: number;
|
|
29
|
+
}): Promise<Loopback>;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
3
|
+
import { TosError } from "../errors.js";
|
|
4
|
+
const PAGE = (title, detail) => `<!doctype html><meta charset="utf-8"><title>${title}</title>` +
|
|
5
|
+
`<body style="font:16px/1.5 system-ui;margin:4rem auto;max-width:28rem;text-align:center">` +
|
|
6
|
+
`<h1 style="font-size:1.25rem">${title}</h1><p style="color:#555">${detail}</p></body>`;
|
|
7
|
+
export async function startLoopback(options) {
|
|
8
|
+
const state = randomBytes(32).toString("base64url");
|
|
9
|
+
let settle;
|
|
10
|
+
let fail;
|
|
11
|
+
const wait = new Promise((resolve, reject) => {
|
|
12
|
+
settle = resolve;
|
|
13
|
+
fail = reject;
|
|
14
|
+
});
|
|
15
|
+
let done = false;
|
|
16
|
+
const server = createServer((req, res) => {
|
|
17
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
18
|
+
if (url.pathname !== "/callback") {
|
|
19
|
+
res.writeHead(404).end();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (done) {
|
|
23
|
+
// A second arrival is not the client: the browser was answered already.
|
|
24
|
+
res.writeHead(409, { "Content-Type": "text/html; charset=utf-8" });
|
|
25
|
+
res.end(PAGE("Already answered", "This sign-in has already completed."));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const error = url.searchParams.get("error");
|
|
29
|
+
if (error) {
|
|
30
|
+
done = true;
|
|
31
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
32
|
+
res.end(PAGE("Not connected", url.searchParams.get("error_description") ?? error));
|
|
33
|
+
server.close();
|
|
34
|
+
fail(new TosError(url.searchParams.get("error_description") ?? `Authorization was refused (${error}).`, "LOGIN_REFUSED"));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const returned = url.searchParams.get("state") ?? "";
|
|
38
|
+
const code = url.searchParams.get("code") ?? "";
|
|
39
|
+
const iss = url.searchParams.get("iss");
|
|
40
|
+
if (!equal(returned, state) || !code) {
|
|
41
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
42
|
+
res.end(PAGE("Not connected", "This response did not match the request."));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// `iss` is how a client holding authorizations from several servers knows
|
|
46
|
+
// which one answered. Ours promises it, so a mismatch is worth refusing.
|
|
47
|
+
if (iss && new URL(iss).origin !== new URL(options.issuer).origin) {
|
|
48
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
49
|
+
res.end(PAGE("Not connected", "This response came from another server."));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
done = true;
|
|
53
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
54
|
+
res.end(PAGE("Connected", "You can close this tab and go back to the terminal."));
|
|
55
|
+
server.close();
|
|
56
|
+
settle({ code, state: returned });
|
|
57
|
+
});
|
|
58
|
+
await new Promise((resolve, reject) => {
|
|
59
|
+
server.once("error", reject);
|
|
60
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
61
|
+
});
|
|
62
|
+
const address = server.address();
|
|
63
|
+
if (!address || typeof address === "string") {
|
|
64
|
+
server.close();
|
|
65
|
+
throw new TosError("Could not open a local port to sign in on.", "LOGIN_FAILED");
|
|
66
|
+
}
|
|
67
|
+
const timer = setTimeout(() => {
|
|
68
|
+
if (done)
|
|
69
|
+
return;
|
|
70
|
+
done = true;
|
|
71
|
+
server.close();
|
|
72
|
+
fail(new TosError("Timed out waiting for the browser.", "LOGIN_TIMEOUT", [
|
|
73
|
+
"implto-tos-axi login --token <paste a key from Settings, Personal credentials>",
|
|
74
|
+
]));
|
|
75
|
+
}, options.timeoutMs);
|
|
76
|
+
timer.unref?.();
|
|
77
|
+
// The promise a caller actually awaits, built once so the handler below
|
|
78
|
+
// attaches to the same object. A caller normally awaits it a moment after
|
|
79
|
+
// starting the flow, so a refusal arriving in between would surface as an
|
|
80
|
+
// unhandled rejection and, in a strict runtime, take the process down.
|
|
81
|
+
// Attaching a handler marks it handled without consuming it: it still
|
|
82
|
+
// rejects for whoever awaits it.
|
|
83
|
+
const settled = wait.finally(() => clearTimeout(timer));
|
|
84
|
+
settled.catch(() => undefined);
|
|
85
|
+
return {
|
|
86
|
+
redirectUri: `http://127.0.0.1:${address.port}/callback`,
|
|
87
|
+
state,
|
|
88
|
+
wait: settled,
|
|
89
|
+
// Settles the promise as well as closing the port. A `close` that only
|
|
90
|
+
// stopped listening left `wait` pending forever, so an abandoned sign-in
|
|
91
|
+
// hung whatever was awaiting it instead of failing.
|
|
92
|
+
close: () => {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
server.close();
|
|
95
|
+
if (done)
|
|
96
|
+
return;
|
|
97
|
+
done = true;
|
|
98
|
+
fail(new TosError("The sign-in was abandoned.", "LOGIN_ABANDONED"));
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/** Constant time, and never throwing on a length the caller chose. */
|
|
103
|
+
function equal(a, b) {
|
|
104
|
+
const left = Buffer.from(a);
|
|
105
|
+
const right = Buffer.from(b);
|
|
106
|
+
if (left.length !== right.length)
|
|
107
|
+
return false;
|
|
108
|
+
return timingSafeEqual(left, right);
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=loopback.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loopback.js","sourceRoot":"","sources":["../../../src/auth/loopback.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hands a URL to whatever opens links on this machine.
|
|
3
|
+
*
|
|
4
|
+
* No dependency for three lines of `spawn`, and deliberately fire-and-forget:
|
|
5
|
+
* the URL is printed too, so a failure to open anything is survivable - the
|
|
6
|
+
* person pastes it. Detached and with stdio ignored so a browser that stays
|
|
7
|
+
* open does not hold the CLI's exit.
|
|
8
|
+
*/
|
|
9
|
+
export declare function openBrowser(url: string): void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { platform } from "node:os";
|
|
3
|
+
/**
|
|
4
|
+
* Hands a URL to whatever opens links on this machine.
|
|
5
|
+
*
|
|
6
|
+
* No dependency for three lines of `spawn`, and deliberately fire-and-forget:
|
|
7
|
+
* the URL is printed too, so a failure to open anything is survivable - the
|
|
8
|
+
* person pastes it. Detached and with stdio ignored so a browser that stays
|
|
9
|
+
* open does not hold the CLI's exit.
|
|
10
|
+
*/
|
|
11
|
+
export function openBrowser(url) {
|
|
12
|
+
const [command, args] = platform() === "darwin"
|
|
13
|
+
? ["open", [url]]
|
|
14
|
+
: platform() === "win32"
|
|
15
|
+
? ["cmd", ["/c", "start", "", url]]
|
|
16
|
+
: ["xdg-open", [url]];
|
|
17
|
+
try {
|
|
18
|
+
spawn(command, args, { detached: true, stdio: "ignore" }).unref();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// The URL is on screen either way.
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=open-browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"open-browser.js","sourceRoot":"","sources":["../../../src/auth/open-browser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,GACnB,QAAQ,EAAE,KAAK,QAAQ;QACrB,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC;QACjB,CAAC,CAAC,QAAQ,EAAE,KAAK,OAAO;YACtB,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC;YACnC,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5B,IAAI,CAAC;QACH,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;IACpE,CAAC;IAAC,MAAM,CAAC;QACP,mCAAmC;IACrC,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { OAuthClientInformationMixed, OAuthClientMetadata, OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js";
|
|
2
|
+
import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
3
|
+
import type { Profile } from "../config/profiles.js";
|
|
4
|
+
/**
|
|
5
|
+
* The SDK's OAuth client, backed by the credentials file.
|
|
6
|
+
*
|
|
7
|
+
* Everything hard is the SDK's: discovery, registration, PKCE, the exchange and
|
|
8
|
+
* the renewal on a 401. What is here is only where the pieces are kept and how
|
|
9
|
+
* the browser is opened - the parts that are this machine's business.
|
|
10
|
+
*
|
|
11
|
+
* `redirectUrl` is the loopback address the listener is already on, so the
|
|
12
|
+
* registration and the authorization request name the same port this process is
|
|
13
|
+
* holding. The server matches loopback with the port ignored, so a registration
|
|
14
|
+
* made this morning still works this afternoon on a different port.
|
|
15
|
+
*/
|
|
16
|
+
export declare class FileAuthProvider implements OAuthClientProvider {
|
|
17
|
+
private readonly profile;
|
|
18
|
+
private readonly redirect;
|
|
19
|
+
private readonly oauthState;
|
|
20
|
+
private readonly scope;
|
|
21
|
+
/** Called when the browser is sent somewhere, so `login` can print it. */
|
|
22
|
+
private readonly onRedirect;
|
|
23
|
+
private verifier;
|
|
24
|
+
constructor(profile: Profile, redirect: string, oauthState: string, scope: string,
|
|
25
|
+
/** Called when the browser is sent somewhere, so `login` can print it. */
|
|
26
|
+
onRedirect: (url: URL) => void);
|
|
27
|
+
get redirectUrl(): string;
|
|
28
|
+
get clientMetadata(): OAuthClientMetadata;
|
|
29
|
+
state(): string;
|
|
30
|
+
clientInformation(): OAuthClientInformationMixed | undefined;
|
|
31
|
+
saveClientInformation(info: OAuthClientInformationMixed): void;
|
|
32
|
+
tokens(): OAuthTokens | undefined;
|
|
33
|
+
saveTokens(tokens: OAuthTokens): void;
|
|
34
|
+
redirectToAuthorization(url: URL): void;
|
|
35
|
+
saveCodeVerifier(verifier: string): void;
|
|
36
|
+
codeVerifier(): string;
|
|
37
|
+
private read;
|
|
38
|
+
private merge;
|
|
39
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { hostname } from "node:os";
|
|
2
|
+
import { readCredential, writeCredential } from "../config/credentials.js";
|
|
3
|
+
import { openBrowser } from "./open-browser.js";
|
|
4
|
+
/**
|
|
5
|
+
* The SDK's OAuth client, backed by the credentials file.
|
|
6
|
+
*
|
|
7
|
+
* Everything hard is the SDK's: discovery, registration, PKCE, the exchange and
|
|
8
|
+
* the renewal on a 401. What is here is only where the pieces are kept and how
|
|
9
|
+
* the browser is opened - the parts that are this machine's business.
|
|
10
|
+
*
|
|
11
|
+
* `redirectUrl` is the loopback address the listener is already on, so the
|
|
12
|
+
* registration and the authorization request name the same port this process is
|
|
13
|
+
* holding. The server matches loopback with the port ignored, so a registration
|
|
14
|
+
* made this morning still works this afternoon on a different port.
|
|
15
|
+
*/
|
|
16
|
+
export class FileAuthProvider {
|
|
17
|
+
profile;
|
|
18
|
+
redirect;
|
|
19
|
+
oauthState;
|
|
20
|
+
scope;
|
|
21
|
+
onRedirect;
|
|
22
|
+
verifier;
|
|
23
|
+
constructor(profile, redirect, oauthState, scope,
|
|
24
|
+
/** Called when the browser is sent somewhere, so `login` can print it. */
|
|
25
|
+
onRedirect) {
|
|
26
|
+
this.profile = profile;
|
|
27
|
+
this.redirect = redirect;
|
|
28
|
+
this.oauthState = oauthState;
|
|
29
|
+
this.scope = scope;
|
|
30
|
+
this.onRedirect = onRedirect;
|
|
31
|
+
}
|
|
32
|
+
get redirectUrl() {
|
|
33
|
+
return this.redirect;
|
|
34
|
+
}
|
|
35
|
+
get clientMetadata() {
|
|
36
|
+
return {
|
|
37
|
+
client_name: `implto-tos-axi on ${hostname()}`,
|
|
38
|
+
redirect_uris: [this.redirect],
|
|
39
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
40
|
+
response_types: ["code"],
|
|
41
|
+
// A CLI ships to laptops and cannot keep a secret. PKCE is what binds a
|
|
42
|
+
// code to this process; a client secret would be theatre.
|
|
43
|
+
token_endpoint_auth_method: "none",
|
|
44
|
+
scope: this.scope,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
state() {
|
|
48
|
+
return this.oauthState;
|
|
49
|
+
}
|
|
50
|
+
clientInformation() {
|
|
51
|
+
const stored = this.read().client;
|
|
52
|
+
return stored ? stored : undefined;
|
|
53
|
+
}
|
|
54
|
+
saveClientInformation(info) {
|
|
55
|
+
this.merge({ client: info });
|
|
56
|
+
}
|
|
57
|
+
tokens() {
|
|
58
|
+
const stored = this.read().tokens;
|
|
59
|
+
if (!stored?.access_token)
|
|
60
|
+
return undefined;
|
|
61
|
+
return {
|
|
62
|
+
access_token: stored.access_token,
|
|
63
|
+
token_type: "Bearer",
|
|
64
|
+
...(stored.refresh_token ? { refresh_token: stored.refresh_token } : {}),
|
|
65
|
+
...(stored.expires_at
|
|
66
|
+
? { expires_in: Math.max(0, Math.round((stored.expires_at - Date.now()) / 1000)) }
|
|
67
|
+
: {}),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
saveTokens(tokens) {
|
|
71
|
+
this.merge({
|
|
72
|
+
tokens: {
|
|
73
|
+
access_token: tokens.access_token,
|
|
74
|
+
...(tokens.refresh_token ? { refresh_token: tokens.refresh_token } : {}),
|
|
75
|
+
...(tokens.expires_in ? { expires_at: Date.now() + tokens.expires_in * 1000 } : {}),
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
redirectToAuthorization(url) {
|
|
80
|
+
this.onRedirect(url);
|
|
81
|
+
openBrowser(url.toString());
|
|
82
|
+
}
|
|
83
|
+
saveCodeVerifier(verifier) {
|
|
84
|
+
// In memory only, and only for this process: a verifier outlives nothing.
|
|
85
|
+
// Writing it down would put the other half of the proof on disk beside the
|
|
86
|
+
// token it protects.
|
|
87
|
+
this.verifier = verifier;
|
|
88
|
+
}
|
|
89
|
+
codeVerifier() {
|
|
90
|
+
if (!this.verifier)
|
|
91
|
+
throw new Error("No PKCE verifier for this sign-in.");
|
|
92
|
+
return this.verifier;
|
|
93
|
+
}
|
|
94
|
+
read() {
|
|
95
|
+
return readCredential(this.profile.name) ?? { origin: this.profile.origin };
|
|
96
|
+
}
|
|
97
|
+
merge(patch) {
|
|
98
|
+
writeCredential(this.profile.name, {
|
|
99
|
+
...this.read(),
|
|
100
|
+
origin: this.profile.origin,
|
|
101
|
+
mcpUrl: this.profile.mcpUrl,
|
|
102
|
+
...patch,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../../../src/auth/provider.ts"],"names":[],"mappings":"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"}
|