argus-decision-mcp 1.2.0 → 1.3.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 +44 -60
- package/SECURITY.md +3 -3
- package/dist/a0/account-connect.js +180 -0
- package/dist/a0/account-credentials.js +86 -0
- package/dist/index.js +35 -1
- package/dist/lib/ambient-elicit.js +225 -0
- package/dist/lib/argus-dir.js +26 -8
- package/dist/lib/calendar.js +2 -2
- package/dist/lib/due-note.js +3 -3
- package/dist/lib/elicit.js +13 -1
- package/dist/lib/ledger-replay.js +19 -3
- package/dist/lib/locale-mismatch.js +52 -0
- package/dist/lib/locale.js +81 -10
- package/dist/lib/localize-result.js +120 -0
- package/dist/lib/localized-message.js +13 -0
- package/dist/lib/premises.js +0 -2
- package/dist/lib/push-account.js +4 -3
- package/dist/lib/review/extract-core.js +116 -0
- package/dist/lib/review/extract-file-node.js +145 -29
- package/dist/lib/review/index.js +1 -1
- package/dist/lib/review/ingest.js +84 -20
- package/dist/lib/review/lenses.js +18 -0
- package/dist/lib/review/prompts.js +199 -22
- package/dist/lib/review/reviewability.js +8 -7
- package/dist/lib/review/routing.js +38 -17
- package/dist/lib/review-path.js +2 -2
- package/dist/lib/spine.js +13 -21
- package/dist/lib/state-machine.js +12 -12
- package/dist/lib/surfaces.js +90 -94
- package/dist/lib/tool-presentation.js +35 -0
- package/dist/resources.js +52 -14
- package/dist/server.js +85 -31
- package/dist/tools/candidates.js +3 -3
- package/dist/tools/check-in.js +24 -12
- package/dist/tools/errors.js +17 -3
- package/dist/tools/index.js +34 -2
- package/dist/tools/init-config.js +39 -11
- package/dist/tools/open-decision.js +58 -25
- package/dist/tools/premises.js +18 -18
- package/dist/tools/public-tools.js +382 -0
- package/dist/tools/recall.js +99 -24
- package/dist/tools/recheck.js +3 -3
- package/dist/tools/review.js +133 -29
- package/dist/tools/seal.js +62 -26
- package/dist/tools/semantic-record.js +225 -0
- package/dist/tools/settle.js +49 -8
- package/dist/tools/sync.js +2 -2
- package/dist/tools/tool-types.js +174 -0
- package/dist/tools/watch.js +1 -1
- package/dist/v2/bridge.js +2 -2
- package/dist/v2/brief.js +1 -1
- package/dist/v2/candidate-capture.js +150 -0
- package/dist/v2/capture-cli.js +72 -0
- package/dist/v2/capture-runtime.js +73 -0
- package/dist/v2/connection-io.js +47 -0
- package/dist/v2/connection.js +93 -0
- package/dist/v2/evidence.js +5 -1
- package/dist/v2/harvest.js +26 -51
- package/dist/v2/lifecycle-cli.js +50 -0
- package/dist/v2/lifecycle.js +298 -2
- package/dist/v2/logbook.js +14 -14
- package/dist/v2/mirror.js +2 -2
- package/dist/v2/queue.js +71 -12
- package/dist/v2/v1-reader.js +6 -3
- package/dist/v3/fixtures/dkk-corpus.js +55 -0
- package/dist/v3/fixtures/p5-measurement-plan.js +42 -0
- package/dist/v3/index.js +5 -0
- package/dist/v3/legacy-v2.js +169 -0
- package/dist/v3/p5-gate.js +123 -0
- package/dist/v3/reducer.js +290 -0
- package/dist/v3/store.js +133 -0
- package/dist/v3/types.js +193 -0
- package/dist/v4/index.js +5 -0
- package/dist/v4/reducer.js +361 -0
- package/dist/v4/relation-validation.js +40 -0
- package/dist/v4/shadow.js +22 -0
- package/dist/v4/types.js +326 -0
- package/dist/v4/watch.js +18 -0
- package/package.json +6 -3
- package/dist/lib/discipline.js +0 -42
- package/dist/prompts.js +0 -72
- package/snippets/claude-code-watch.md +0 -47
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/argus-decision-mcp)
|
|
5
5
|
[](./LICENSE)
|
|
6
6
|
|
|
7
|
+
> **Keeping Judgment Human.**
|
|
8
|
+
|
|
7
9
|
> **Your AI gives you an answer. Argus gives you a receipt — and checks it against reality on the date you set.**
|
|
8
10
|
|
|
9
11
|
Argus is an MCP server for **decision accountability**. Instead of grading your
|
|
@@ -19,7 +21,7 @@ connectors that require an HTTP transport aren't supported yet — see the roadm
|
|
|
19
21
|
|
|
20
22
|
```
|
|
21
23
|
┌─ ARGUS · JUDGMENT RECEIPT ────────────────────────────────┐
|
|
22
|
-
|
|
24
|
+
Prediction saved 2026-04-02 Result recorded 2026-06-30
|
|
23
25
|
|
|
24
26
|
THE REAL QUESTION
|
|
25
27
|
Can we cut over without a maintenance window users notice?
|
|
@@ -33,7 +35,7 @@ connectors that require an HTTP transport aren't supported yet — see the roadm
|
|
|
33
35
|
─────────────────────────────────────────────────────────
|
|
34
36
|
AI VERDICT ON THIS DECISION ······················ NONE
|
|
35
37
|
The model never graded you. Reality did.
|
|
36
|
-
|
|
38
|
+
└────────────────────────── argus · prediction → reality ─┘
|
|
37
39
|
```
|
|
38
40
|
|
|
39
41
|
## Why it's different
|
|
@@ -52,8 +54,8 @@ promised in prose:
|
|
|
52
54
|
|
|
53
55
|
- **There is no verdict tool.** The model cannot grade your decision because no
|
|
54
56
|
`argus_verdict` / `argus_score` tool exists to call. `grep dist/` and see.
|
|
55
|
-
- **You can't
|
|
56
|
-
a prior `
|
|
57
|
+
- **You can't record a result without a saved prediction.** `argus_resolve`
|
|
58
|
+
hard-errors without a prior `argus_predict` — "no judgment without a falsifiable prediction" is a
|
|
57
59
|
precondition, not a suggestion.
|
|
58
60
|
- **State is the ledger, not a flag.** A decision's status is the fold of an
|
|
59
61
|
append-only event log, so it can't be faked by calling tools out of order.
|
|
@@ -130,96 +132,77 @@ fails to start, use:
|
|
|
130
132
|
|
|
131
133
|
You never call these tools by name. Once the server is connected, you just
|
|
132
134
|
talk to your AI in plain language and it calls the right tool for you. One
|
|
133
|
-
full loop — from
|
|
135
|
+
full loop — from saving a prediction to recording what reality did — looks like
|
|
134
136
|
this:
|
|
135
137
|
|
|
136
|
-
**1 ·
|
|
138
|
+
**1 · Save a prediction** — before you commit to a decision, tell your AI:
|
|
137
139
|
|
|
138
|
-
> **You:** "I'm shipping the new onboarding flow next week.
|
|
140
|
+
> **You:** "I'm shipping the new onboarding flow next week. Save this prediction:
|
|
139
141
|
> signups go up at least 10% by the end of the month."
|
|
140
142
|
|
|
141
143
|
Argus records your predicate and a check-by date. Nothing is scored — it's a
|
|
142
144
|
prediction against reality, not a grade from the model.
|
|
143
145
|
|
|
144
|
-
**2 ·
|
|
146
|
+
**2 · Update a fact** *(optional, any time before the date)* — if a fact
|
|
145
147
|
your decision leaned on might have moved:
|
|
146
148
|
|
|
147
|
-
> **You:** "
|
|
148
|
-
>
|
|
149
|
+
> **You:** "Update the signup number behind my onboarding decision. Has it
|
|
150
|
+
> changed since I saved the prediction?"
|
|
149
151
|
|
|
150
152
|
Argus compares the new number to the baseline and tells you if it drifted. It
|
|
151
153
|
returns the handle — whether to revisit the decision stays your call.
|
|
152
154
|
|
|
153
|
-
**3 ·
|
|
155
|
+
**3 · Record the result on the check-by date** — when the date arrives:
|
|
154
156
|
|
|
155
|
-
> **You:** "
|
|
157
|
+
> **You:** "Record the result of my onboarding prediction. Signups went up 14%."
|
|
156
158
|
|
|
157
159
|
Argus prints the **Judgment Receipt** at the top of this page — with
|
|
158
|
-
`AI VERDICT … NONE`. You made the call;
|
|
160
|
+
`AI VERDICT … NONE`. You made the call; the record contains what reality did,
|
|
161
|
+
not a model's grade.
|
|
159
162
|
|
|
160
163
|
That's the whole spine. Everything below is detail on top of these three steps.
|
|
161
164
|
|
|
162
|
-
##
|
|
163
|
-
|
|
164
|
-
Argus runs two loops side by side — the same three habits its author
|
|
165
|
-
uses daily: **봉인하기** (seal it before you ask the AI) · **드러내기**
|
|
166
|
-
(surface claims you accepted without checking) · **돌아보기** (return when
|
|
167
|
-
reality has answered):
|
|
165
|
+
## The everyday loop
|
|
168
166
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
usually evaporate: a claim you accepted without checking, a premise you
|
|
173
|
-
haven't verified, a question you're deferring. Tomorrow's `check_in` mirrors
|
|
174
|
-
yesterday's line back — *"so, how did it go?"* — as a question, never a grade.
|
|
175
|
-
An anchor is a **note, not a prediction**: nothing about it is scored, counted, or
|
|
176
|
-
streaked, ever.
|
|
177
|
-
- **The decision loop** (everything below) — plays out over days to months.
|
|
178
|
-
When a capture turns out to be load-bearing, *you* promote it: open the
|
|
179
|
-
decision, seal a falsifiable prediction, track its premises, and let reality
|
|
180
|
-
settle it into a Judgment Receipt.
|
|
167
|
+
Argus now exposes six tools named for the job they do. You do not initialize
|
|
168
|
+
it first, choose a ritual, or learn its internal state machine. The first useful
|
|
169
|
+
call creates the local record automatically.
|
|
181
170
|
|
|
182
|
-
|
|
171
|
+
For most decisions the loop is simply:
|
|
183
172
|
|
|
184
|
-
|
|
173
|
+
1. **`argus_capture`** — capture a decision's premises and open questions, in your own words.
|
|
174
|
+
2. **`argus_predict`** — make a falsifiable prediction and the date reality can answer it.
|
|
175
|
+
3. **`argus_check_in`** — see only what needs attention now.
|
|
176
|
+
4. **`argus_resolve`** — record what actually happened, without a score or verdict.
|
|
185
177
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
these three, Argus works. Everything else in the table supports that spine —
|
|
190
|
-
reach for it when you need it, not before.
|
|
178
|
+
`argus_patterns` reads what is already on record — past decisions and how often
|
|
179
|
+
your predictions held. `argus_settings` handles the few preferences and
|
|
180
|
+
account-sync controls a user may need.
|
|
191
181
|
|
|
192
182
|
| Tool | What it does |
|
|
193
183
|
|------|--------------|
|
|
194
|
-
| `
|
|
195
|
-
| `
|
|
196
|
-
| `
|
|
197
|
-
| `
|
|
198
|
-
| `
|
|
199
|
-
| `
|
|
200
|
-
| `argus_settle` | On the check-by date, records what reality did and issues the Judgment Receipt. Hard-errors without a prior seal. Optionally records **which premise broke** (your attribution, never inferred) — over time your track record can say "3 of your 4 missed bets traced to a broken external premise": a frequency, never a diagnosis. |
|
|
201
|
-
| `argus_amend` | Changes the predicate or check-by date **before** reality answers — a course change, not an erasure (the original stays on the append-only ledger). Hard-errors once the decision is settled. |
|
|
202
|
-
| `argus_dismiss` | Closes a decision **without settling** — it became irrelevant, was decided elsewhere, or you changed your mind. No verdict is recorded; terminal, not reopened. |
|
|
203
|
-
| `argus_check_in` | Returns contracts past their check-by date **and premise facts due for a re-check** (the same fact under several decisions is one re-check), and mirrors yesterday's watch anchor back as a question. If nothing is due, it says so and stops — it doesn't manufacture engagement. |
|
|
204
|
-
| `argus_sync` | Pulls your account receipts into the terminal (live judgments + what's due) so you can settle here. Seals push automatically; this is the read side. Requires `ARGUS_TOKEN`. |
|
|
205
|
-
| `argus_recall` | Reads your own history: a receipt, the open contracts, or a sample-size-caveated track record (never a tier or score). |
|
|
206
|
-
| `argus_init` / `argus_config` | Initialize the `.argus` directory; read/write non-spine settings. |
|
|
184
|
+
| `argus_capture` | Captures one decision's premises and open questions in the user's own words, without deciding for you. Its actions add assumptions and open questions, record your answer, update an external fact, change an untested prediction, or close a decision that no longer needs an outcome. |
|
|
185
|
+
| `argus_predict` | Makes a falsifiable prediction (`predicate` + `check_by`) in the user's words. An Argus-drafted line is marked honestly and shown as a one-tap draft to keep, reword, or skip. |
|
|
186
|
+
| `argus_check_in` | Shows only predictions past their check date, external facts due for an update, and open questions due for reconsideration. If nothing needs attention, it stops. |
|
|
187
|
+
| `argus_resolve` | Records what actually happened and issues the Judgment Receipt. Reality supplies the outcome; Argus does not grade it. |
|
|
188
|
+
| `argus_patterns` | Reads active decisions, all records, one Judgment Receipt, one decision's context, or the accumulated frequency of outcomes. Read-only. |
|
|
189
|
+
| `argus_settings` | Reads or updates language, quiet reminders, opt-in premise sync, and explicit account sync. Initialization is automatic. |
|
|
207
190
|
|
|
208
191
|
## Living premises
|
|
209
192
|
|
|
210
193
|
The receipt's sharpest line — `THE UNVERIFIED ASSUMPTION` — used to be written
|
|
211
|
-
once
|
|
194
|
+
once when a prediction was saved and then go dead. Now it's a **tracked object**: name the premises
|
|
212
195
|
a decision rests on, correct the ones the model got wrong (your edit is part of
|
|
213
196
|
the record), and re-check the load-bearing external facts against reality while
|
|
214
197
|
the bet is still open. When a rate hike breaks the premise under three of your
|
|
215
198
|
decisions, that's **one** re-check, not three.
|
|
216
199
|
|
|
217
200
|
Honest limits, stated up front: an MCP server is passive — nothing wakes it
|
|
218
|
-
between
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
201
|
+
between saving a prediction and recording its result. Every tool response can
|
|
202
|
+
carry a quiet `due_note`, `argus_check_in` reports due items, and the single
|
|
203
|
+
`argus://attention` resource lets capable hosts auto-inject what needs attention. No
|
|
204
|
+
separate prompt ritual is advertised. Anything more periodic (cron, reminders)
|
|
205
|
+
belongs to your host or habits, not this server.
|
|
223
206
|
A premise that never gets re-checked shows up honestly as `never re-checked` —
|
|
224
207
|
Argus does not pretend liveness.
|
|
225
208
|
|
|
@@ -229,14 +212,15 @@ Everything is local, under `.argus/` in your project (gitignored by default) —
|
|
|
229
212
|
an append-only `ledger.jsonl` **you own**: plain JSON lines, no lock-in,
|
|
230
213
|
receipts render to shareable text. **No telemetry by default.** The only network
|
|
231
214
|
call Argus makes out of the box is the opt-in account sync: if — and only if —
|
|
232
|
-
you set `ARGUS_TOKEN`, a
|
|
215
|
+
you set `ARGUS_TOKEN`, a saved prediction or recorded result is POSTed to your own Argus
|
|
233
216
|
account so it can email you at its check-by date. Separately, you can opt in to
|
|
234
217
|
**anonymous** usage telemetry with `ARGUS_TELEMETRY=1` (a random install id +
|
|
235
218
|
which tool ran + version/platform, never your decisions or token; honors
|
|
236
219
|
`DO_NOT_TRACK`) — see SECURITY.md. **Premise data stays on your machine
|
|
237
220
|
by default** — it is not part of the sync payload. There is exactly one switch
|
|
238
|
-
that changes this: `
|
|
239
|
-
|
|
221
|
+
that changes this: `argus_settings` with `action:"update"` and
|
|
222
|
+
`premise_sync:true` (off unless you set it)
|
|
223
|
+
sends a saved decision's *monitored* premises along, so your account's
|
|
240
224
|
autonomous premise-watch can re-check them against reality and email you when
|
|
241
225
|
one materially moves. Unset the token and Argus never touches the network.
|
|
242
226
|
See [SECURITY.md](SECURITY.md).
|
package/SECURITY.md
CHANGED
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
Telemetry rows are **deleted automatically after 90 days** (a daily database
|
|
20
20
|
job — retention is enforced by the database itself, not by policy prose). The
|
|
21
21
|
runtime dependency surface is intentionally small and visible in
|
|
22
|
-
`package.json`: MCP SDK, schema/config helpers, and document parsers
|
|
23
|
-
`
|
|
24
|
-
- **Private by default.**
|
|
22
|
+
`package.json`: MCP SDK, schema/config helpers, and document parsers.
|
|
23
|
+
`npm audit --omit=dev` should stay clean before publishing.
|
|
24
|
+
- **Private by default.** On first use Argus adds `sessions/`, `ledger/`,
|
|
25
25
|
`config.yaml`, and `.bound` to `.argus/.gitignore` so your decisions and
|
|
26
26
|
local settings are never committed accidentally.
|
|
27
27
|
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { randomBytes, createHash } from 'node:crypto';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { writeStoredAccountCredential, } from './account-credentials.js';
|
|
5
|
+
const DEFAULT_API_URL = 'https://argus.voyage';
|
|
6
|
+
const CONNECT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
7
|
+
function apiBase(value) {
|
|
8
|
+
const url = new URL(value.replace(/\/+$/, ''));
|
|
9
|
+
const local = url.hostname === '127.0.0.1' || url.hostname === 'localhost' || url.hostname === '::1';
|
|
10
|
+
if (url.protocol !== 'https:' && !(url.protocol === 'http:' && local)) {
|
|
11
|
+
throw new Error('ARGUS_API_URL must use HTTPS (HTTP is allowed only for localhost).');
|
|
12
|
+
}
|
|
13
|
+
return url.toString().replace(/\/+$/, '');
|
|
14
|
+
}
|
|
15
|
+
function base64url(bytes = 32) {
|
|
16
|
+
return randomBytes(bytes).toString('base64url');
|
|
17
|
+
}
|
|
18
|
+
function challenge(verifier) {
|
|
19
|
+
return createHash('sha256').update(verifier).digest('base64url');
|
|
20
|
+
}
|
|
21
|
+
async function defaultOpenExternal(url) {
|
|
22
|
+
const command = process.platform === 'win32' ? 'explorer.exe' : process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
23
|
+
try {
|
|
24
|
+
const child = spawn(command, [url], { detached: true, stdio: 'ignore', windowsHide: true });
|
|
25
|
+
child.unref();
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function listen(server) {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
server.once('error', reject);
|
|
35
|
+
server.listen(0, '127.0.0.1', () => resolve(server.address().port));
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function close(server) {
|
|
39
|
+
return new Promise((resolve) => server.close(() => resolve()));
|
|
40
|
+
}
|
|
41
|
+
function waitForCode(server, expectedState) {
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
const timeout = setTimeout(() => reject(new Error('Account connection timed out.')), CONNECT_TIMEOUT_MS);
|
|
44
|
+
server.on('request', (request, response) => {
|
|
45
|
+
const url = new URL(request.url || '/', 'http://127.0.0.1');
|
|
46
|
+
if (url.pathname !== '/callback') {
|
|
47
|
+
response.writeHead(404).end('Not found');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const code = url.searchParams.get('code');
|
|
51
|
+
const state = url.searchParams.get('state');
|
|
52
|
+
if (!code || state !== expectedState) {
|
|
53
|
+
response.writeHead(400, { 'content-type': 'text/plain; charset=utf-8' }).end('Invalid or expired Argus connection.');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
clearTimeout(timeout);
|
|
57
|
+
response.writeHead(200, {
|
|
58
|
+
'content-type': 'text/html; charset=utf-8',
|
|
59
|
+
'content-security-policy': "default-src 'none'; style-src 'unsafe-inline'",
|
|
60
|
+
'cache-control': 'no-store',
|
|
61
|
+
}).end('<!doctype html><meta charset="utf-8"><title>Argus connected</title><style>body{font:16px system-ui;max-width:38rem;margin:15vh auto;padding:2rem;color:#202020}h1{font-size:1.4rem}</style><h1>Argus account connected</h1><p>You can close this window and return to your terminal.</p>');
|
|
62
|
+
resolve(code);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async function requestJson(fetchImpl, url, body) {
|
|
67
|
+
const response = await fetchImpl(url, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: { 'content-type': 'application/json' },
|
|
70
|
+
body: JSON.stringify(body),
|
|
71
|
+
});
|
|
72
|
+
let json = {};
|
|
73
|
+
try {
|
|
74
|
+
json = await response.json();
|
|
75
|
+
}
|
|
76
|
+
catch { /* surfaced as protocol failure below */ }
|
|
77
|
+
return { response, json };
|
|
78
|
+
}
|
|
79
|
+
function tokenFrom(json) {
|
|
80
|
+
if (typeof json.access_token !== 'string' || !json.access_token.startsWith('argus_pat_'))
|
|
81
|
+
throw new Error('The account returned an invalid credential.');
|
|
82
|
+
if (json.token_type !== 'Bearer' || typeof json.scope !== 'string' || typeof json.expires_at !== 'string')
|
|
83
|
+
throw new Error('The account returned an incomplete credential.');
|
|
84
|
+
return json;
|
|
85
|
+
}
|
|
86
|
+
function persist(token, base, credentialFile) {
|
|
87
|
+
const stored = {
|
|
88
|
+
version: 1,
|
|
89
|
+
access_token: token.access_token,
|
|
90
|
+
token_type: 'Bearer',
|
|
91
|
+
scope: token.scope,
|
|
92
|
+
expires_at: token.expires_at,
|
|
93
|
+
api_url: base,
|
|
94
|
+
connected_at: new Date().toISOString(),
|
|
95
|
+
};
|
|
96
|
+
writeStoredAccountCredential(stored, credentialFile);
|
|
97
|
+
return stored;
|
|
98
|
+
}
|
|
99
|
+
async function connectWithBrowser(options, base) {
|
|
100
|
+
const verifier = base64url(48);
|
|
101
|
+
const state = base64url(24);
|
|
102
|
+
const server = createServer();
|
|
103
|
+
const port = await listen(server);
|
|
104
|
+
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
105
|
+
const codePromise = waitForCode(server, state);
|
|
106
|
+
const authorize = new URL(`${base}/en/auth/callback/mcp-connect`);
|
|
107
|
+
authorize.searchParams.set('response_type', 'code');
|
|
108
|
+
authorize.searchParams.set('redirect_uri', redirectUri);
|
|
109
|
+
authorize.searchParams.set('state', state);
|
|
110
|
+
authorize.searchParams.set('code_challenge', challenge(verifier));
|
|
111
|
+
authorize.searchParams.set('code_challenge_method', 'S256');
|
|
112
|
+
authorize.searchParams.set('client_name', options.clientName || 'Argus MCP');
|
|
113
|
+
options.writeLine('Opening Argus in your browser to approve this device…');
|
|
114
|
+
const opened = await options.openExternal(authorize.toString());
|
|
115
|
+
if (!opened)
|
|
116
|
+
options.writeLine(`Open this URL in a browser:\n${authorize.toString()}`);
|
|
117
|
+
try {
|
|
118
|
+
const code = await codePromise;
|
|
119
|
+
const { response, json } = await requestJson(options.fetchImpl, `${base}/api/mcp/oauth/token`, {
|
|
120
|
+
grant_type: 'authorization_code',
|
|
121
|
+
code,
|
|
122
|
+
code_verifier: verifier,
|
|
123
|
+
redirect_uri: redirectUri,
|
|
124
|
+
});
|
|
125
|
+
if (!response.ok)
|
|
126
|
+
throw new Error(`Account token exchange failed (${String(json.error || response.status)}).`);
|
|
127
|
+
return persist(tokenFrom(json), base, options.credentialFile);
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
await close(server);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function connectWithDeviceCode(options, base) {
|
|
134
|
+
const { response, json } = await requestJson(options.fetchImpl, `${base}/api/mcp/oauth/device`, { client_name: options.clientName || 'Argus MCP' });
|
|
135
|
+
if (!response.ok)
|
|
136
|
+
throw new Error(`Could not start device authorization (${String(json.error || response.status)}).`);
|
|
137
|
+
if (typeof json.device_code !== 'string' || typeof json.user_code !== 'string' || typeof json.verification_uri !== 'string') {
|
|
138
|
+
throw new Error('The account returned an invalid device authorization.');
|
|
139
|
+
}
|
|
140
|
+
let interval = typeof json.interval === 'number' ? Math.max(1, json.interval) : 5;
|
|
141
|
+
const expiresIn = typeof json.expires_in === 'number' ? json.expires_in : 600;
|
|
142
|
+
const deadline = Date.now() + expiresIn * 1000;
|
|
143
|
+
options.writeLine(`Open ${String(json.verification_uri)} and enter code: ${json.user_code}`);
|
|
144
|
+
while (Date.now() < deadline) {
|
|
145
|
+
await options.sleep(interval * 1000);
|
|
146
|
+
const poll = await requestJson(options.fetchImpl, `${base}/api/mcp/oauth/token`, {
|
|
147
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
148
|
+
device_code: json.device_code,
|
|
149
|
+
});
|
|
150
|
+
if (poll.response.ok)
|
|
151
|
+
return persist(tokenFrom(poll.json), base, options.credentialFile);
|
|
152
|
+
if (poll.json.error === 'authorization_pending')
|
|
153
|
+
continue;
|
|
154
|
+
if (poll.json.error === 'slow_down') {
|
|
155
|
+
interval += 5;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (poll.json.error === 'access_denied')
|
|
159
|
+
throw new Error('Account connection was denied.');
|
|
160
|
+
if (poll.json.error === 'expired_token')
|
|
161
|
+
break;
|
|
162
|
+
throw new Error(`Device authorization failed (${String(poll.json.error || poll.response.status)}).`);
|
|
163
|
+
}
|
|
164
|
+
throw new Error('Device authorization expired. Start the connection again.');
|
|
165
|
+
}
|
|
166
|
+
export async function connectAccount(options = {}) {
|
|
167
|
+
const base = apiBase(options.apiUrl || process.env.ARGUS_API_URL || DEFAULT_API_URL);
|
|
168
|
+
const shared = {
|
|
169
|
+
...options,
|
|
170
|
+
fetchImpl: options.fetchImpl || fetch,
|
|
171
|
+
openExternal: options.openExternal || defaultOpenExternal,
|
|
172
|
+
writeLine: options.writeLine || ((line) => process.stdout.write(`${line}\n`)),
|
|
173
|
+
sleep: options.sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
|
|
174
|
+
};
|
|
175
|
+
const credential = options.headless
|
|
176
|
+
? await connectWithDeviceCode(shared, base)
|
|
177
|
+
: await connectWithBrowser(shared, base);
|
|
178
|
+
shared.writeLine(`Connected. Credential stored locally; expires ${credential.expires_at.slice(0, 10)}.`);
|
|
179
|
+
return credential;
|
|
180
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
function defaultCredentialPath() {
|
|
5
|
+
const explicit = process.env.ARGUS_ACCOUNT_FILE?.trim();
|
|
6
|
+
if (explicit)
|
|
7
|
+
return path.resolve(explicit);
|
|
8
|
+
const argusDir = process.env.ARGUS_DIR?.trim();
|
|
9
|
+
if (argusDir)
|
|
10
|
+
return path.join(path.resolve(argusDir), 'account-credentials.json');
|
|
11
|
+
if (process.platform === 'win32') {
|
|
12
|
+
const root = process.env.LOCALAPPDATA?.trim() || path.join(os.homedir(), 'AppData', 'Local');
|
|
13
|
+
return path.join(root, 'Argus', 'account-credentials.json');
|
|
14
|
+
}
|
|
15
|
+
if (process.platform === 'darwin') {
|
|
16
|
+
return path.join(os.homedir(), 'Library', 'Application Support', 'Argus', 'account-credentials.json');
|
|
17
|
+
}
|
|
18
|
+
const root = process.env.XDG_CONFIG_HOME?.trim() || path.join(os.homedir(), '.config');
|
|
19
|
+
return path.join(root, 'argus', 'account-credentials.json');
|
|
20
|
+
}
|
|
21
|
+
export function accountCredentialPath() {
|
|
22
|
+
return defaultCredentialPath();
|
|
23
|
+
}
|
|
24
|
+
export function readStoredAccountCredential(file = defaultCredentialPath()) {
|
|
25
|
+
// Unit tests must opt into an isolated temp location. Never probe a
|
|
26
|
+
// developer's actual config directory merely because a sync helper ran.
|
|
27
|
+
if (process.env.NODE_ENV === 'test' && !process.env.ARGUS_ACCOUNT_FILE && !process.env.ARGUS_DIR && file === defaultCredentialPath()) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
32
|
+
if (parsed.version !== 1 || parsed.token_type !== 'Bearer')
|
|
33
|
+
return null;
|
|
34
|
+
if (typeof parsed.access_token !== 'string' || !parsed.access_token.startsWith('argus_pat_'))
|
|
35
|
+
return null;
|
|
36
|
+
if (typeof parsed.expires_at !== 'string' || Date.parse(parsed.expires_at) <= Date.now())
|
|
37
|
+
return null;
|
|
38
|
+
if (typeof parsed.api_url !== 'string' || typeof parsed.scope !== 'string' || typeof parsed.connected_at !== 'string')
|
|
39
|
+
return null;
|
|
40
|
+
return parsed;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function writeStoredAccountCredential(credential, file = defaultCredentialPath()) {
|
|
47
|
+
const dir = path.dirname(file);
|
|
48
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
49
|
+
const temp = path.join(dir, `.${path.basename(file)}.${process.pid}.${Date.now()}.tmp`);
|
|
50
|
+
try {
|
|
51
|
+
fs.writeFileSync(temp, `${JSON.stringify(credential, null, 2)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
52
|
+
fs.renameSync(temp, file);
|
|
53
|
+
try {
|
|
54
|
+
fs.chmodSync(file, 0o600);
|
|
55
|
+
}
|
|
56
|
+
catch { /* Windows ACLs do not use POSIX modes. */ }
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
try {
|
|
60
|
+
fs.unlinkSync(temp);
|
|
61
|
+
}
|
|
62
|
+
catch { /* rename already removed it */ }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Manual ARGUS_TOKEN remains the advanced/CI override. */
|
|
66
|
+
export function resolveAccountToken() {
|
|
67
|
+
const manual = process.env.ARGUS_TOKEN?.trim();
|
|
68
|
+
if (manual)
|
|
69
|
+
return manual;
|
|
70
|
+
return readStoredAccountCredential()?.access_token ?? '';
|
|
71
|
+
}
|
|
72
|
+
export function resolveAccountApiUrl() {
|
|
73
|
+
const manual = process.env.ARGUS_API_URL?.trim();
|
|
74
|
+
if (manual)
|
|
75
|
+
return manual;
|
|
76
|
+
return readStoredAccountCredential()?.api_url ?? 'https://argus.voyage';
|
|
77
|
+
}
|
|
78
|
+
export function disconnectStoredAccount(file = defaultCredentialPath()) {
|
|
79
|
+
try {
|
|
80
|
+
fs.unlinkSync(file);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
return error.code === 'ENOENT';
|
|
85
|
+
}
|
|
86
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
import { createServer } from './server.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
async function main() {
|
|
5
|
+
if (process.argv[2] === 'connect') {
|
|
6
|
+
const { connectAccount } = await import('./a0/account-connect.js');
|
|
7
|
+
await connectAccount({ headless: process.argv.includes('--headless') });
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
if (process.argv[2] === 'disconnect') {
|
|
11
|
+
const { disconnectStoredAccount } = await import('./a0/account-credentials.js');
|
|
12
|
+
disconnectStoredAccount();
|
|
13
|
+
process.stdout.write('Local Argus account credential removed. Revoke the device in Settings to invalidate its server token.\n');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (process.argv[2] === 'capture-scan') {
|
|
17
|
+
const { runCaptureCli } = await import('./v2/capture-cli.js');
|
|
18
|
+
await runCaptureCli(process.argv.slice(3));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (process.argv[2] === 'capture-status') {
|
|
22
|
+
const { runCaptureStatusCli } = await import('./v2/capture-cli.js');
|
|
23
|
+
runCaptureStatusCli(process.argv.slice(3));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (process.argv[2] === 'capture-purge') {
|
|
27
|
+
const { runCapturePurgeCli } = await import('./v2/capture-cli.js');
|
|
28
|
+
runCapturePurgeCli(process.argv.slice(3));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (['archive-export', 'archive-restore', 'local-purge'].includes(process.argv[2] ?? '')) {
|
|
32
|
+
const { runLifecycleCli } = await import('./v2/lifecycle-cli.js');
|
|
33
|
+
runLifecycleCli(process.argv[2], process.argv.slice(3));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
5
36
|
const server = await createServer();
|
|
6
37
|
const transport = new StdioServerTransport();
|
|
7
38
|
await server.connect(transport);
|
|
@@ -10,4 +41,7 @@ async function main() {
|
|
|
10
41
|
process.exit(0);
|
|
11
42
|
});
|
|
12
43
|
}
|
|
13
|
-
main().catch(
|
|
44
|
+
main().catch((error) => {
|
|
45
|
+
console.error(error);
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
});
|