argus-decision-mcp 1.0.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.
Files changed (113) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +135 -41
  3. package/SECURITY.md +22 -8
  4. package/dist/a0/account-connect.js +180 -0
  5. package/dist/a0/account-credentials.js +86 -0
  6. package/dist/index.js +35 -1
  7. package/dist/lib/ambient-due.js +63 -0
  8. package/dist/lib/ambient-elicit.js +225 -0
  9. package/dist/lib/argus-dir.js +76 -9
  10. package/dist/lib/calendar.js +75 -0
  11. package/dist/lib/canonical-scales.js +105 -0
  12. package/dist/lib/due-note.js +62 -13
  13. package/dist/lib/elicit.js +13 -1
  14. package/dist/lib/envelope.js +19 -4
  15. package/dist/lib/install-id.js +53 -0
  16. package/dist/lib/layout.js +7 -0
  17. package/dist/lib/ledger-append.js +134 -3
  18. package/dist/lib/ledger-replay.js +139 -11
  19. package/dist/lib/locale-mismatch.js +52 -0
  20. package/dist/lib/locale.js +120 -4
  21. package/dist/lib/localize-result.js +120 -0
  22. package/dist/lib/localized-message.js +13 -0
  23. package/dist/lib/log.js +2 -2
  24. package/dist/lib/numeric-drift.js +343 -22
  25. package/dist/lib/package-meta.js +14 -0
  26. package/dist/lib/premise-sync.js +23 -0
  27. package/dist/lib/premises-core.js +174 -0
  28. package/dist/lib/premises.js +51 -50
  29. package/dist/lib/privacy.js +1 -1
  30. package/dist/lib/push-account.js +28 -4
  31. package/dist/lib/receipt.js +5 -1
  32. package/dist/lib/render-receipt.js +32 -22
  33. package/dist/lib/resolve-today.js +11 -3
  34. package/dist/lib/review/extract-core.js +345 -0
  35. package/dist/lib/review/extract-file-node.js +288 -0
  36. package/dist/lib/review/index.js +1 -1
  37. package/dist/lib/review/ingest.js +84 -20
  38. package/dist/lib/review/lenses.js +18 -0
  39. package/dist/lib/review/prompts.js +254 -15
  40. package/dist/lib/review/render.js +86 -38
  41. package/dist/lib/review/reviewability.js +8 -7
  42. package/dist/lib/review/routing.js +38 -17
  43. package/dist/lib/review/schema.js +4 -1
  44. package/dist/lib/review-path.js +108 -0
  45. package/dist/lib/spine.js +16 -12
  46. package/dist/lib/state-machine.js +59 -13
  47. package/dist/lib/surface-lint.js +97 -0
  48. package/dist/lib/surfaces.js +313 -48
  49. package/dist/lib/telemetry.js +169 -0
  50. package/dist/lib/tool-presentation.js +35 -0
  51. package/dist/lib/untrusted.js +89 -0
  52. package/dist/lib/validate-crux.js +32 -7
  53. package/dist/resources.js +52 -14
  54. package/dist/server.js +105 -42
  55. package/dist/tools/amend-dismiss.js +46 -9
  56. package/dist/tools/candidates.js +129 -0
  57. package/dist/tools/check-in.js +178 -20
  58. package/dist/tools/errors.js +17 -3
  59. package/dist/tools/index.js +36 -2
  60. package/dist/tools/init-config.js +83 -11
  61. package/dist/tools/open-decision.js +71 -36
  62. package/dist/tools/premises.js +246 -29
  63. package/dist/tools/public-tools.js +382 -0
  64. package/dist/tools/recall.js +158 -19
  65. package/dist/tools/recheck.js +51 -15
  66. package/dist/tools/review.js +234 -33
  67. package/dist/tools/seal.js +167 -31
  68. package/dist/tools/semantic-record.js +225 -0
  69. package/dist/tools/settle.js +204 -20
  70. package/dist/tools/sync.js +245 -13
  71. package/dist/tools/tool-types.js +174 -0
  72. package/dist/tools/watch.js +117 -0
  73. package/dist/v2/bridge.js +240 -0
  74. package/dist/v2/brief.js +122 -0
  75. package/dist/v2/candidate-capture.js +150 -0
  76. package/dist/v2/capture-cli.js +72 -0
  77. package/dist/v2/capture-runtime.js +73 -0
  78. package/dist/v2/connection-io.js +47 -0
  79. package/dist/v2/connection.js +93 -0
  80. package/dist/v2/events.js +187 -0
  81. package/dist/v2/evidence.js +55 -0
  82. package/dist/v2/gate-keywords.js +48 -0
  83. package/dist/v2/gate.js +98 -0
  84. package/dist/v2/git-discovery.js +80 -0
  85. package/dist/v2/harvest.js +122 -0
  86. package/dist/v2/init.js +42 -0
  87. package/dist/v2/ledger.js +290 -0
  88. package/dist/v2/lifecycle-cli.js +50 -0
  89. package/dist/v2/lifecycle.js +418 -0
  90. package/dist/v2/logbook.js +142 -0
  91. package/dist/v2/mirror.js +305 -0
  92. package/dist/v2/outbox.js +52 -0
  93. package/dist/v2/queue.js +183 -0
  94. package/dist/v2/reducer.js +462 -0
  95. package/dist/v2/sanitize.js +44 -0
  96. package/dist/v2/v1-reader.js +300 -0
  97. package/dist/v3/fixtures/dkk-corpus.js +55 -0
  98. package/dist/v3/fixtures/p5-measurement-plan.js +42 -0
  99. package/dist/v3/index.js +5 -0
  100. package/dist/v3/legacy-v2.js +169 -0
  101. package/dist/v3/p5-gate.js +123 -0
  102. package/dist/v3/reducer.js +290 -0
  103. package/dist/v3/store.js +133 -0
  104. package/dist/v3/types.js +193 -0
  105. package/dist/v4/index.js +5 -0
  106. package/dist/v4/reducer.js +361 -0
  107. package/dist/v4/relation-validation.js +40 -0
  108. package/dist/v4/shadow.js +22 -0
  109. package/dist/v4/types.js +326 -0
  110. package/dist/v4/watch.js +18 -0
  111. package/package.json +20 -6
  112. package/dist/lib/discipline.js +0 -42
  113. package/dist/prompts.js +0 -72
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Argus
3
+ Copyright (c) 2026 commet
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # argus-decision-mcp
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/argus-decision-mcp.svg)](https://www.npmjs.com/package/argus-decision-mcp)
4
+ [![npm downloads](https://img.shields.io/npm/dm/argus-decision-mcp.svg)](https://www.npmjs.com/package/argus-decision-mcp)
5
+ [![license](https://img.shields.io/npm/l/argus-decision-mcp.svg)](./LICENSE)
6
+
7
+ > **Keeping Judgment Human.**
8
+
3
9
  > **Your AI gives you an answer. Argus gives you a receipt — and checks it against reality on the date you set.**
4
10
 
5
11
  Argus is an MCP server for **decision accountability**. Instead of grading your
@@ -9,11 +15,13 @@ what actually happened. The artifact it produces — a **Judgment Receipt** —
9
15
  carries one line no other AI tool will: `AI VERDICT … NONE`. The model never
10
16
  graded you. Reality did.
11
17
 
12
- Works with any MCP host Claude, ChatGPT, Gemini, or any MCP-compatible client.
18
+ Runs on any MCP host that supports local **stdio** servers Claude Desktop,
19
+ Claude Code, and other clients that launch a local process. (Remote-only
20
+ connectors that require an HTTP transport aren't supported yet — see the roadmap.)
13
21
 
14
22
  ```
15
23
  ┌─ ARGUS · JUDGMENT RECEIPT ────────────────────────────────┐
16
- Sealed 2026-04-02 Settled 2026-06-30
24
+ Prediction saved 2026-04-02 Result recorded 2026-06-30
17
25
 
18
26
  THE REAL QUESTION
19
27
  Can we cut over without a maintenance window users notice?
@@ -27,19 +35,27 @@ Works with any MCP host — Claude, ChatGPT, Gemini, or any MCP-compatible clien
27
35
  ─────────────────────────────────────────────────────────
28
36
  AI VERDICT ON THIS DECISION ······················ NONE
29
37
  The model never graded you. Reality did.
30
- └────────────────────────────────── argus · sealsettle ─┘
38
+ └────────────────────────── argus · predictionreality ─┘
31
39
  ```
32
40
 
33
41
  ## Why it's different
34
42
 
43
+ **It is not a receipt of what your *agent* did.** A growing set of tools now
44
+ log an AI coding run — prompt captured, files touched, checkpoint saved, replay
45
+ path written down. That receipt is about the *machine's* actions over one run.
46
+ Argus receipts something no run-logger can: *your* judgment call, in your own
47
+ words, opened again on a date you set and settled against **reality** — not
48
+ against a model's opinion, and not against a diff. Machine-action receipts and
49
+ judgment receipts are different primitives; you can keep both.
50
+
35
51
  Most decision tools compete on a *better answer*, a *score*, a *confidence*.
36
52
  Argus does the opposite, and the opposite is enforced **structurally**, not
37
53
  promised in prose:
38
54
 
39
55
  - **There is no verdict tool.** The model cannot grade your decision because no
40
56
  `argus_verdict` / `argus_score` tool exists to call. `grep dist/` and see.
41
- - **You can't settle what you never sealed.** `argus_settle` hard-errors without
42
- a prior `argus_seal` — "no judgment without a falsifiable bet" is a
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
43
59
  precondition, not a suggestion.
44
60
  - **State is the ledger, not a flag.** A decision's status is the fold of an
45
61
  append-only event log, so it can't be faked by calling tools out of order.
@@ -52,7 +68,10 @@ Claude Code:
52
68
  claude mcp add argus -- npx -y argus-decision-mcp
53
69
  ```
54
70
 
55
- Or add to your host's MCP config:
71
+ Or add to your host's MCP config. **Zero config works**: with no `env` at all,
72
+ your ledger lives in `~/.argus`.
73
+
74
+ **Claude Code** (expands `${CLAUDE_PROJECT_DIR}`, so a per-project ledger works):
56
75
 
57
76
  ```jsonc
58
77
  {
@@ -61,19 +80,21 @@ Or add to your host's MCP config:
61
80
  "command": "npx",
62
81
  "args": ["-y", "argus-decision-mcp"],
63
82
  "env": {
64
- // Set this ONCE and you never pass argus_dir again — every tool falls
65
- // back to it. Claude Code expands ${CLAUDE_PROJECT_DIR}; on other hosts
66
- // put an absolute path here (or pass argus_dir per call).
83
+ // OPTIONAL per-project ledger. Omit entirely to use ~/.argus.
67
84
  "ARGUS_DIR": "${CLAUDE_PROJECT_DIR}/.argus",
68
85
  // OPTIONAL — connect to your Argus account so sealed predictions get an
69
86
  // email at their check-by date (the Companion Brief) and show up in the
70
- // web dashboard. Issue the token in the web app. Leave it unset to stay
71
- // fully local (the privacy-preserving default).
87
+ // web dashboard. Issue the token in the web app (Settings sync token).
88
+ // Leave it unset to stay fully local (the privacy-preserving default).
72
89
  "ARGUS_TOKEN": "argus_pat_…",
73
90
  // OPTIONAL — the timezone that decides when a check-by date becomes
74
- // "today". Unset = UTC, which for Korean users means the day flips at
75
- // 9am KST (a due decision won't show until then). Set your zone:
76
- "ARGUS_TZ": "Asia/Seoul"
91
+ // "today". Unset = your machine's local timezone (usually right).
92
+ // Set it only if your machine's clock zone isn't where you live:
93
+ "ARGUS_TZ": "Asia/Seoul",
94
+ // OPTIONAL — opt in to anonymous usage telemetry (OFF by default). Sends
95
+ // a random install id + which tool ran + version/platform; never your
96
+ // decisions or token. Honors DO_NOT_TRACK. See SECURITY.md → Telemetry.
97
+ "ARGUS_TELEMETRY": "1"
77
98
  // "ARGUS_API_URL": "https://argus.voyage" // override only for self-host
78
99
  }
79
100
  }
@@ -81,42 +102,107 @@ Or add to your host's MCP config:
81
102
  }
82
103
  ```
83
104
 
105
+ **Claude Desktop** does **not** expand `${...}` variables — a literal
106
+ `${CLAUDE_PROJECT_DIR}` would fail on every call (Argus names this error when
107
+ it happens). Either omit `ARGUS_DIR` (→ `~/.argus`) or use an absolute path:
108
+
109
+ ```jsonc
110
+ // macOS
111
+ "env": { "ARGUS_DIR": "/Users/you/.argus" }
112
+ // Windows — also note the command form below
113
+ "env": { "ARGUS_DIR": "C:\\Users\\you\\.argus" }
114
+ ```
115
+
116
+ **Windows** hosts often can't launch bare `npx` (it's `npx.cmd`). If the server
117
+ fails to start, use:
118
+
119
+ ```jsonc
120
+ {
121
+ "command": "cmd",
122
+ "args": ["/c", "npx", "-y", "argus-decision-mcp"]
123
+ }
124
+ ```
125
+
84
126
  > `argus_dir` is **optional** on every tool: omit it and it resolves from
85
- > `ARGUS_DIR`. A per-call `argus_dir` still wins — so Argus works on any host
86
- > even when env-variable interpolation doesn't.
127
+ > `ARGUS_DIR`, then falls back to `~/.argus`. A per-call `argus_dir` still
128
+ > wins — so Argus works on any host even when env-variable interpolation
129
+ > doesn't.
130
+
131
+ ## Your first receipt (2 minutes)
132
+
133
+ You never call these tools by name. Once the server is connected, you just
134
+ talk to your AI in plain language and it calls the right tool for you. One
135
+ full loop — from saving a prediction to recording what reality did — looks like
136
+ this:
137
+
138
+ **1 · Save a prediction** — before you commit to a decision, tell your AI:
139
+
140
+ > **You:** "I'm shipping the new onboarding flow next week. Save this prediction:
141
+ > signups go up at least 10% by the end of the month."
142
+
143
+ Argus records your predicate and a check-by date. Nothing is scored — it's a
144
+ prediction against reality, not a grade from the model.
145
+
146
+ **2 · Update a fact** *(optional, any time before the date)* — if a fact
147
+ your decision leaned on might have moved:
148
+
149
+ > **You:** "Update the signup number behind my onboarding decision. Has it
150
+ > changed since I saved the prediction?"
151
+
152
+ Argus compares the new number to the baseline and tells you if it drifted. It
153
+ returns the handle — whether to revisit the decision stays your call.
154
+
155
+ **3 · Record the result on the check-by date** — when the date arrives:
156
+
157
+ > **You:** "Record the result of my onboarding prediction. Signups went up 14%."
158
+
159
+ Argus prints the **Judgment Receipt** at the top of this page — with
160
+ `AI VERDICT … NONE`. You made the call; the record contains what reality did,
161
+ not a model's grade.
162
+
163
+ That's the whole spine. Everything below is detail on top of these three steps.
164
+
165
+ ## The everyday loop
166
+
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.
170
+
171
+ For most decisions the loop is simply:
172
+
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.
87
177
 
88
- ## The loop
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.
89
181
 
90
182
  | Tool | What it does |
91
183
  |------|--------------|
92
- | `argus_open_decision` | Opens a consequential decision. Runs a restraint gate first on a flat / low-stakes / reversible / already-closed call it tells you to leave it as is. If it fires, it surfaces **one** neutral question, never a fork or a lean. |
93
- | `argus_review` | Reviews an existing document (strategy memo / PRD / deck text / AI answer) for judgment risk: reviewability score, routed lenses, source units with anchors, and the extraction prompt then hands the analysis to you. Degrades honestly on unextractable input; never a verdict. End by sealing one follow-up. |
94
- | `argus_seal` | Seals a falsifiable prediction (`predicate` + `check_by`) and captures the receipt's real-question / unverified-assumption / human-only / your-call fields. Refuses an empty predicate or a non-future date. If you seal without naming the assumption, it's recorded as an explicit **skip** — never a forced gate (which would just eject the tiredest user), never a silent blank. With `ARGUS_TOKEN` set, the prediction also syncs to your account so the Companion Brief can email you at its check-by date. |
95
- | `argus_premises` | Tracks the **premises** a decision rests on — the facts and open questions behind it. `add` records them (echoed back in full); `amend` takes your correction verbatim (the AI's original stays on the record — the edit is the signal); `resolve` closes an open question **in your own words** (it cannot offer options or leans — that shape doesn't exist here). Premises lock once the check-by arrives: no retroactive premise-planting, no retiring the one about to be proven wrong. |
96
- | `argus_recheck` | Re-checks one premise against reality between seal and settle. The host researches the current fact; the tool compares **explicit numbers** mechanically (never regex-parses prose) or records the host's provenance-tagged `changed` assertion for text facts. First check is a baseline and never alerts. When the fact drifted: it says so and returns the handle — whether to revisit the decision stays your call. `apply_to_matching` re-checks the same fact under your other decisions at once. |
97
- | `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. |
98
- | `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. |
99
- | `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. |
100
- | `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). If nothing is due, it says so and stops — it doesn't manufacture engagement. |
101
- | `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`. |
102
- | `argus_recall` | Reads your own history: a receipt, the open contracts, or a sample-size-caveated track record (never a tier or score). |
103
- | `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. |
104
190
 
105
191
  ## Living premises
106
192
 
107
193
  The receipt's sharpest line — `THE UNVERIFIED ASSUMPTION` — used to be written
108
- once at seal and then go dead. Now it's a **tracked object**: name the premises
194
+ once when a prediction was saved and then go dead. Now it's a **tracked object**: name the premises
109
195
  a decision rests on, correct the ones the model got wrong (your edit is part of
110
196
  the record), and re-check the load-bearing external facts against reality while
111
197
  the bet is still open. When a rate hike breaks the premise under three of your
112
198
  decisions, that's **one** re-check, not three.
113
199
 
114
200
  Honest limits, stated up front: an MCP server is passive — nothing wakes it
115
- between seal and settle. The return loop rides four levers: every tool response
116
- carries a quiet `due_note`; `argus_check_in` reports due premises;
117
- the `argus://premises/due` resource lets hosts auto-inject them; and the
118
- `/argus-settle` ritual includes the re-check choreography. Anything more
119
- periodic (cron, reminders) belongs to your host or habits, not this server.
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.
120
206
  A premise that never gets re-checked shows up honestly as `never re-checked` —
121
207
  Argus does not pretend liveness.
122
208
 
@@ -124,12 +210,20 @@ Argus does not pretend liveness.
124
210
 
125
211
  Everything is local, under `.argus/` in your project (gitignored by default) —
126
212
  an append-only `ledger.jsonl` **you own**: plain JSON lines, no lock-in,
127
- receipts render to shareable text. No telemetry. The **only** network call
128
- Argus ever makes is the opt-in account sync: if — and only if — you set
129
- `ARGUS_TOKEN`, a sealed/settled prediction is POSTed to your own Argus account
130
- so it can email you at its check-by date. **Premise data never leaves your
131
- machine** it is not part of the sync payload. Unset the token and Argus never
132
- touches the network. See [SECURITY.md](SECURITY.md).
213
+ receipts render to shareable text. **No telemetry by default.** The only network
214
+ call Argus makes out of the box is the opt-in account sync: if — and only if —
215
+ you set `ARGUS_TOKEN`, a saved prediction or recorded result is POSTed to your own Argus
216
+ account so it can email you at its check-by date. Separately, you can opt in to
217
+ **anonymous** usage telemetry with `ARGUS_TELEMETRY=1` (a random install id +
218
+ which tool ran + version/platform, never your decisions or token; honors
219
+ `DO_NOT_TRACK`) — see SECURITY.md. **Premise data stays on your machine
220
+ by default** — it is not part of the sync payload. There is exactly one switch
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
224
+ autonomous premise-watch can re-check them against reality and email you when
225
+ one materially moves. Unset the token and Argus never touches the network.
226
+ See [SECURITY.md](SECURITY.md).
133
227
 
134
228
  ## Measured
135
229
 
package/SECURITY.md CHANGED
@@ -2,14 +2,28 @@
2
2
 
3
3
  ## Data & privacy
4
4
 
5
- - **All data is local.** Argus writes only under the `.argus/` directory you
6
- point it at: an append-only `ledger/ledger.jsonl`, per-decision
7
- `sessions/{id}/` files, and a `config.yaml`. Nothing leaves the machine.
8
- - **No telemetry, no network.** The runtime dependency surface is two packages
9
- (`@modelcontextprotocol/sdk`, `js-yaml`). `npm ls --prod` should show no
10
- analytics or network client. CI fails if the production dependency tree grows.
11
- - **Private by default.** `argus_init` adds `sessions/`, `ledger/`, and `.bound`
12
- to `.argus/.gitignore` so your decisions are never committed.
5
+ - **All data is local by default.** Argus writes only under the `.argus/`
6
+ directory you point it at: an append-only `ledger/ledger.jsonl`,
7
+ per-decision `sessions/{id}/` files, and a `config.yaml`. Nothing leaves the
8
+ machine unless you explicitly set `ARGUS_TOKEN` for account sync (or opt in to
9
+ anonymous telemetry with `ARGUS_TELEMETRY=1`; see below).
10
+ - **No telemetry by default.** With no `ARGUS_TOKEN` and no `ARGUS_TELEMETRY=1`,
11
+ the server makes no network calls. Telemetry is strictly **opt-in**: set
12
+ `ARGUS_TELEMETRY=1` and it sends an anonymous, content-free ping — a random,
13
+ machine-local install id (`~/.argus/.telemetry-id`, not tied to your account
14
+ or token, regenerable by deleting the file), which of the built-in tools ran
15
+ and whether it succeeded, plus the package version, coarse OS platform, and
16
+ Node major. It **never** sends decision content, titles, predicates, file
17
+ paths, `argus_dir`, or the account token. `DO_NOT_TRACK=1` disables it even if
18
+ the flag is set, and the sink stores no `user_id` (nothing to identify).
19
+ Telemetry rows are **deleted automatically after 90 days** (a daily database
20
+ job — retention is enforced by the database itself, not by policy prose). The
21
+ runtime dependency surface is intentionally small and visible in
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
+ `config.yaml`, and `.bound` to `.argus/.gitignore` so your decisions and
26
+ local settings are never committed accidentally.
13
27
 
14
28
  ## Threat model
15
29
 
@@ -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(console.error);
44
+ main().catch((error) => {
45
+ console.error(error);
46
+ process.exitCode = 1;
47
+ });