@needmoretruth/nmts-cli 0.17.2

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 (151) hide show
  1. package/AGENTS.md +591 -0
  2. package/LICENSE +202 -0
  3. package/LICENSING.md +49 -0
  4. package/README.ko.md +641 -0
  5. package/README.md +657 -0
  6. package/dist/account-proof.js +78 -0
  7. package/dist/account.js +75 -0
  8. package/dist/api-key.js +310 -0
  9. package/dist/api.js +271 -0
  10. package/dist/args.js +137 -0
  11. package/dist/artifact-about.js +70 -0
  12. package/dist/autonomy.js +98 -0
  13. package/dist/code-access.js +76 -0
  14. package/dist/code-vault.js +233 -0
  15. package/dist/collision.js +133 -0
  16. package/dist/commands/balance.js +93 -0
  17. package/dist/commands/consent.js +77 -0
  18. package/dist/commands/create.js +356 -0
  19. package/dist/commands/env.js +136 -0
  20. package/dist/commands/expiring.js +167 -0
  21. package/dist/commands/extend.js +236 -0
  22. package/dist/commands/get.js +130 -0
  23. package/dist/commands/kit.js +136 -0
  24. package/dist/commands/listfile.js +105 -0
  25. package/dist/commands/login.js +335 -0
  26. package/dist/commands/logout.js +33 -0
  27. package/dist/commands/ls.js +202 -0
  28. package/dist/commands/marks.js +176 -0
  29. package/dist/commands/mcp.js +139 -0
  30. package/dist/commands/mode.js +54 -0
  31. package/dist/commands/on-collision.js +45 -0
  32. package/dist/commands/organise.js +296 -0
  33. package/dist/commands/public-code.js +89 -0
  34. package/dist/commands/pull.js +185 -0
  35. package/dist/commands/push.js +278 -0
  36. package/dist/commands/put.js +282 -0
  37. package/dist/commands/rebuild.js +160 -0
  38. package/dist/commands/receive.js +125 -0
  39. package/dist/commands/recovery-list.js +104 -0
  40. package/dist/commands/recovery.js +291 -0
  41. package/dist/commands/s3.js +241 -0
  42. package/dist/commands/settings.js +28 -0
  43. package/dist/commands/share.js +276 -0
  44. package/dist/commands/sweep.js +240 -0
  45. package/dist/commands/trash.js +288 -0
  46. package/dist/commands/trial.js +264 -0
  47. package/dist/commands/update.js +169 -0
  48. package/dist/commands/usage.js +84 -0
  49. package/dist/commands/verify.js +331 -0
  50. package/dist/commands/wallet.js +122 -0
  51. package/dist/commands/whoami.js +33 -0
  52. package/dist/consent.js +200 -0
  53. package/dist/credentials.js +311 -0
  54. package/dist/crypto-surface.js +54 -0
  55. package/dist/crypto.js +137 -0
  56. package/dist/download-part.js +144 -0
  57. package/dist/download-sink.js +213 -0
  58. package/dist/download.js +163 -0
  59. package/dist/drive-paths.js +170 -0
  60. package/dist/environment.js +190 -0
  61. package/dist/errors.js +71 -0
  62. package/dist/exit.js +107 -0
  63. package/dist/expiry.js +115 -0
  64. package/dist/extend-chain.js +137 -0
  65. package/dist/extend-plan.js +149 -0
  66. package/dist/extend-sign.js +130 -0
  67. package/dist/guards.js +15 -0
  68. package/dist/help.js +192 -0
  69. package/dist/human-check.js +54 -0
  70. package/dist/item-trash.js +25 -0
  71. package/dist/kit-file.js +133 -0
  72. package/dist/list-file.js +71 -0
  73. package/dist/list-view-find.js +71 -0
  74. package/dist/list-view-order.js +96 -0
  75. package/dist/main.js +394 -0
  76. package/dist/manifest-create.js +81 -0
  77. package/dist/manifest-write.js +244 -0
  78. package/dist/manifest.js +213 -0
  79. package/dist/mark-render.js +45 -0
  80. package/dist/mcp-args.js +154 -0
  81. package/dist/mcp-tools/context.js +50 -0
  82. package/dist/mcp-tools/files.js +133 -0
  83. package/dist/mcp-tools/organise.js +128 -0
  84. package/dist/mcp-tools/reads.js +89 -0
  85. package/dist/mcp-tools/share.js +61 -0
  86. package/dist/mcp.js +142 -0
  87. package/dist/net-retry.js +84 -0
  88. package/dist/network.js +43 -0
  89. package/dist/notice.js +45 -0
  90. package/dist/product.js +38 -0
  91. package/dist/progress.js +110 -0
  92. package/dist/prompt.js +145 -0
  93. package/dist/rebuild.js +255 -0
  94. package/dist/recovery-assemble.js +148 -0
  95. package/dist/recovery-build.js +260 -0
  96. package/dist/recovery-map-file.js +114 -0
  97. package/dist/recovery-map.js +191 -0
  98. package/dist/recovery-release.js +229 -0
  99. package/dist/recovery-seq.js +63 -0
  100. package/dist/recovery-source.js +143 -0
  101. package/dist/registration.js +83 -0
  102. package/dist/s3/listing.js +134 -0
  103. package/dist/s3/multipart.js +104 -0
  104. package/dist/s3/response-sink.js +41 -0
  105. package/dist/s3/same-file.js +117 -0
  106. package/dist/s3/server.js +252 -0
  107. package/dist/s3/sigv4.js +168 -0
  108. package/dist/s3/staging.js +87 -0
  109. package/dist/s3/xml.js +80 -0
  110. package/dist/safe-path.js +89 -0
  111. package/dist/seal.js +206 -0
  112. package/dist/secret-reader.js +112 -0
  113. package/dist/server.js +33 -0
  114. package/dist/session.js +49 -0
  115. package/dist/setup-questions.js +31 -0
  116. package/dist/share.js +195 -0
  117. package/dist/shared/lib/crypto/size-padding.js +126 -0
  118. package/dist/shared/lib/drive/manifest-codec.js +287 -0
  119. package/dist/shared/lib/drive/manifest-index.js +235 -0
  120. package/dist/shared/lib/drive/manifest-ops.js +267 -0
  121. package/dist/shared/lib/drive/manifest-settings.js +65 -0
  122. package/dist/shared/lib/drive/name-conflict.js +121 -0
  123. package/dist/shared/lib/drive/unique-name.js +48 -0
  124. package/dist/shared/lib/extend/epochs.js +256 -0
  125. package/dist/shared/lib/net/retry-budget.js +95 -0
  126. package/dist/shared/lib/share/shared-file-info.js +65 -0
  127. package/dist/shared/lib/storage-network.js +64 -0
  128. package/dist/shared/lib/upload/part-plan.js +28 -0
  129. package/dist/stdout.js +109 -0
  130. package/dist/trash-sweep.js +123 -0
  131. package/dist/units.js +19 -0
  132. package/dist/update-check.js +164 -0
  133. package/dist/update-source.js +143 -0
  134. package/dist/upload-api.js +98 -0
  135. package/dist/upload-file.js +242 -0
  136. package/dist/upload-price.js +117 -0
  137. package/dist/upload-steps.js +111 -0
  138. package/dist/upload-store.js +252 -0
  139. package/dist/upload-wire.js +25 -0
  140. package/dist/upload.js +294 -0
  141. package/dist/usage-report.js +54 -0
  142. package/dist/wallet-chain.js +76 -0
  143. package/dist/wallet.js +209 -0
  144. package/dist/walrus-write.js +188 -0
  145. package/dist/walrus.js +156 -0
  146. package/package.json +56 -0
  147. package/vendor/nmts-crypto/nmts_crypto_wasm.d.ts +469 -0
  148. package/vendor/nmts-crypto/nmts_crypto_wasm.js +1300 -0
  149. package/vendor/nmts-crypto/nmts_crypto_wasm_bg.wasm +0 -0
  150. package/vendor/nmts-crypto/nmts_crypto_wasm_bg.wasm.d.ts +55 -0
  151. package/vendor/nmts-crypto/package.json +17 -0
@@ -0,0 +1,84 @@
1
+ // Going on across a connection that dropped, changed, or is simply slow.
2
+ //
3
+ // ⛔ THE POLICY IS NOT WRITTEN HERE. It is `shared/lib/net/retry-budget.ts`, copied byte for byte
4
+ // from the browser, so that both programs mean the same thing by "we tried". What IS here is
5
+ // the waiting, because how you wait is different in a terminal than in a page.
6
+ //
7
+ // ⛔ WHAT MAY BE REPEATED, AND WHAT MAY NEVER BE. Repeating a GET costs nothing. Repeating a write
8
+ // whose outcome is unknown can spend money twice -- a request that reached the server and died
9
+ // on the way back looks exactly like one that never arrived. So a write is repeated ONLY when
10
+ // it carries an idempotency key, which is the server's promise that a second copy of the same
11
+ // request is the same request. Everything else fails once and says so.
12
+ //
13
+ // ⛔ NOTHING IS RETRIED SILENTLY. A person running this in a terminal and an agent reading its
14
+ // output both need to know that the tool is waiting rather than stuck; `onWait` is how they are
15
+ // told, and every command that can wait passes something.
16
+ //
17
+ // ⚠ NODE HAS NO "ONLINE" EVENT AND NO ONLINE FLAG. A browser can be told the network came back;
18
+ // here the only way to find out is to ask again. So the offline branch of the policy is not
19
+ // reachable from this file, and the whole budget is the online one -- which is honest: without a
20
+ // signal to wait for, waiting longer is just waiting.
21
+ import { nextAttempt, WATCHED_RETRY_BUDGET_MS } from "./shared/lib/net/retry-budget.js";
22
+ /**
23
+ * How long a command goes on trying before it reports the failure.
24
+ *
25
+ * ⛔ SHORTER THAN ANYTHING ELSE, BECAUSE SOMEBODY TYPED THIS AND IS LOOKING AT IT. The long budget
26
+ * is for work nobody is watching -- a page uploading in the background, which has money already
27
+ * spent on it and resumes where it stopped. A terminal that sits silent is indistinguishable
28
+ * from one that has hung, an agent has a deadline of its own, and running the command again
29
+ * costs nothing. What this has to cover is a link that blinks, not a server that is down.
30
+ */
31
+ export const CLI_RETRY_BUDGET_MS = Math.round(WATCHED_RETRY_BUDGET_MS / 3);
32
+ /** Run `step` until it succeeds or the budget is spent. Rejects with the LAST error. */
33
+ export async function keepTrying(step, options) {
34
+ const now = options.now ?? (() => Date.now());
35
+ const random = options.random ?? Math.random;
36
+ const sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
37
+ let elapsedOnlineMs = 0;
38
+ for (let attempt = 1;; attempt += 1) {
39
+ options.signal?.throwIfAborted();
40
+ try {
41
+ return await step();
42
+ }
43
+ catch (error) {
44
+ if (options.signal?.aborted === true)
45
+ throw error;
46
+ if (!options.retryable(error))
47
+ throw error;
48
+ const plan = nextAttempt({
49
+ attempt,
50
+ elapsedOnlineMs,
51
+ elapsedOfflineMs: 0,
52
+ online: true,
53
+ random: random(),
54
+ budgetMs: options.budgetMs ?? CLI_RETRY_BUDGET_MS,
55
+ });
56
+ if (!plan.again)
57
+ throw error;
58
+ options.onWait?.({ attempt, waitMs: plan.waitMs, error });
59
+ const before = now();
60
+ await sleep(plan.waitMs);
61
+ // The time that actually passed, not the time that was planned: a machine that was suspended
62
+ // mid-wait spent all of it, and charging the planned number would let it wait for ever.
63
+ elapsedOnlineMs += Math.max(0, now() - before);
64
+ }
65
+ }
66
+ }
67
+ /**
68
+ * Is this failure one that asking again could fix?
69
+ *
70
+ * ⛔ A REFUSAL IS NOT A BLIP. The server saying no -- wrong key, no credits, not found -- is an
71
+ * answer, and repeating it spends the budget to hear it again later. Only the shapes that mean
72
+ * "nobody answered" or "not right now" come back here as true.
73
+ */
74
+ export function isTransient(error, status) {
75
+ if (typeof status === "number")
76
+ return status === 408 || status === 429 || status >= 500;
77
+ if (!(error instanceof Error))
78
+ return false;
79
+ // ⛔ A DEADLINE THAT FIRED IS NOT A BLIP. The request already had its thirty seconds; asking
80
+ // again usually spends thirty more and ends the same way, and the deadline exists precisely so
81
+ // that an agent loop is not left waiting. What comes back here as true is a connection that
82
+ // was refused, reset or never made -- which is what moving between networks looks like.
83
+ return /could not reach|fetch failed|network|ECONNRESET|ECONNREFUSED|ECONNABORTED|EAI_AGAIN|ENOTFOUND|socket hang up/i.test(`${error.message} ${String(error.cause ?? "")}`);
84
+ }
@@ -0,0 +1,43 @@
1
+ // Which storage network this run uses — and why it is never guessed.
2
+ //
3
+ // ⛔ THE TRAP THIS EXISTS TO CLOSE. In the web app the network comes from an environment variable
4
+ // read at module load, and an EMPTY value silently means "testnet". A production build refuses
5
+ // to start that way; a Node process does not. So a tool that inherited that default would point
6
+ // a mainnet account at testnet aggregators, testnet relays and a testnet chain check — and
7
+ // would report that everything was fine while looking in a place the files were never in.
8
+ //
9
+ // ⛔ SO THE RULE HERE IS: the live server implies mainnet, and ANY other server must say which
10
+ // network it is. There is no fallback. A tool that has to guess where somebody's files are
11
+ // should stop instead.
12
+ import { NmtsError } from "./errors.js";
13
+ import { DEFAULT_SERVER } from "./server.js";
14
+ export const NETWORKS = ["mainnet", "testnet"];
15
+ export const NETWORK_ENV_VAR = "NMTS_NETWORK";
16
+ function isNetwork(value) {
17
+ return NETWORKS.includes(value);
18
+ }
19
+ /**
20
+ * Decide the network for this run.
21
+ *
22
+ * `explicit` beats the environment, which beats the one inference this function is willing to
23
+ * make: the live server is mainnet. Anything else and it refuses.
24
+ */
25
+ export function resolveNetwork(server, explicit) {
26
+ const stated = explicit ?? process.env[NETWORK_ENV_VAR];
27
+ if (stated !== undefined && stated.length > 0) {
28
+ if (!isNetwork(stated)) {
29
+ throw new NmtsError(`Not a network: ${stated}`, {
30
+ exitCode: 2,
31
+ nextStep: `Use one of: ${NETWORKS.join(", ")}.`,
32
+ });
33
+ }
34
+ return stated;
35
+ }
36
+ if (server === DEFAULT_SERVER)
37
+ return "mainnet";
38
+ throw new NmtsError(`Cannot tell which storage network ${server} uses.`, {
39
+ exitCode: 2,
40
+ nextStep: `Set ${NETWORK_ENV_VAR}=mainnet or ${NETWORK_ENV_VAR}=testnet. ` +
41
+ `Guessing would look for your files on a network they were never stored on.`,
42
+ });
43
+ }
package/dist/notice.js ADDED
@@ -0,0 +1,45 @@
1
+ // The one thing this tool says before it keeps an account code on a machine.
2
+ //
3
+ // ⛔ WHY IT EXISTS. Handing an agent your account code is handing it the vault: in NMTS every key
4
+ // is derived from that one code — the file keys and the wallet both — so an agent that leaks it
5
+ // has leaked everything at once, and the account cannot be re-keyed because the account IS the
6
+ // code. The owner asked for this to be said out loud (2026-08-23).
7
+ //
8
+ // ⛔ WHY IT SAYS FACTS AND NOT "WE ARE NOT LIABLE". A disclaimer is a contract term, and the Terms
9
+ // of Service in force today do not carry one for this. Printing a term we have not published
10
+ // would be claiming an agreement that does not exist. A clause covering this is drafted for the
11
+ // next version of the published Terms; until that version is in force, this text states only
12
+ // what is verifiably true, which is the part that actually helps somebody decide anyway.
13
+ // ⛔ When that version ships, the term belongs in the Terms — not here. This stays facts.
14
+ //
15
+ // ⛔ WHY IT IS SHOWN ONCE AND NOT EVERY RUN. A warning printed on every command is a warning
16
+ // nobody reads, and it would land in the middle of an agent's output forever. It is shown at
17
+ // the moment the decision is made — when the code is first written to this machine.
18
+ import { PRODUCT_NAME } from "./product.js";
19
+ /**
20
+ * The text shown before an account code is stored on this machine.
21
+ *
22
+ * Every sentence here is a measured fact about how NMTS works, not a prediction and not a
23
+ * comparison. If any of it stops being true, this text is wrong and must change with the code.
24
+ */
25
+ export function firstRunNotice() {
26
+ return [
27
+ `${PRODUCT_NAME} is about to keep your account code on this machine.`,
28
+ ``,
29
+ ` Your account code is the only key to your account. The keys that encrypt`,
30
+ ` your files and the keys to your wallet are all derived from it.`,
31
+ ``,
32
+ ` Keeping it here is what lets this tool work while you are away. It also`,
33
+ ` means any program that can read your files can read it, including any`,
34
+ ` agent you run on this machine.`,
35
+ ``,
36
+ ` If it leaves this machine — in a log line, in a prompt, in a repository —`,
37
+ ` whoever holds it can read every file in the account and spend from the`,
38
+ ` wallet. NMTS cannot undo that: requests made with your code cannot be`,
39
+ ` told apart from your own, and the code cannot be changed while keeping`,
40
+ ` the account.`,
41
+ ``,
42
+ ` Give an agent an account you would be willing to lose.`,
43
+ ``,
44
+ ].join("\n");
45
+ }
@@ -0,0 +1,38 @@
1
+ // The product's own names, in one place.
2
+ //
3
+ // WHY A MODULE FOR THREE STRINGS. The name reached the code before it was settled — first
4
+ // "NMTS for Agent", then "NMTS for Agents", and finally `nmts-cli` with the tool itself just
5
+ // called `nmts` (owner, 2026-08-23). A name spelled out at twelve call sites is a name that
6
+ // ends up spelled three ways.
7
+ //
8
+ // ⛔ "for agents" is NOT part of the name. It says who this is for, which belongs in the README
9
+ // and the package description where it can change without renaming anything.
10
+ /** What a person reads, and what the tool calls itself. Owner-settled 2026-08-23. */
11
+ export const PRODUCT_NAME = "nmts";
12
+ /** What an agent types. The same word: there is nothing to translate between the two. */
13
+ export const BINARY_NAME = "nmts";
14
+ /**
15
+ * This build's version.
16
+ *
17
+ * ⛔ Kept here rather than read from `package.json`: the published build has no package.json
18
+ * beside it. Here rather than in `main.ts` because the MCP server has to say it too, and a
19
+ * command importing the entry point is a cycle waiting to bite.
20
+ */
21
+ export const VERSION = "0.17.2";
22
+ /** Where the product lives, for messages that need to send somebody somewhere real. */
23
+ export const HOME_URL = "https://nmts.me";
24
+ /** The source, so a person holding only the built program can find what it was built from. */
25
+ export const SOURCE_URL = "https://github.com/needmoretruth/nmts-cli";
26
+ /** Who holds the copyright. One place, because it is also the answer to "who can license this". */
27
+ export const COPYRIGHT = "Copyright 2026 needmoretruth";
28
+ /**
29
+ * Where to write about a fault, a confusing message, or anything that got in the way.
30
+ *
31
+ * ⛔ IT IS IN THE TOOL AND NOT ONLY IN THE README, because the moment somebody wants to report
32
+ * something is the moment it went wrong -- and that is exactly when nobody goes looking for a
33
+ * web page. The smallest annoyance is worth an email; most of them are cheap to fix and
34
+ * invisible from here.
35
+ */
36
+ export const SUPPORT_EMAIL = "nmts@nmts.me";
37
+ /** The document in the source repository that is written for an agent rather than for a person. */
38
+ export const AGENTS_DOC = "AGENTS.md";
@@ -0,0 +1,110 @@
1
+ // Showing how far along something is, without making a mess of a pipe.
2
+ //
3
+ // ⛔ A TERMINAL AND A PIPE ARE DIFFERENT AUDIENCES. A person watching wants one line that rewrites
4
+ // itself; an agent capturing stdout wants text it can read, and a thousand carriage returns in
5
+ // its buffer is noise it has to strip before it can find the answer. So the same reporter
6
+ // behaves differently depending on where it is writing, and the decision is made once, here.
7
+ //
8
+ // ⛔ IT NEVER WRITES TO STDOUT WHEN THE OUTPUT IS MACHINE-READABLE. `--json` promises one JSON
9
+ // document and nothing else; a progress line would break every parser that trusted that.
10
+ export function stderrSink() {
11
+ return {
12
+ // ⛔ STDERR, NOT STDOUT. Progress is not the answer, and a caller redirecting the answer to a
13
+ // file must not find it interleaved with percentages.
14
+ write: (text) => void process.stderr.write(text),
15
+ interactive: process.stderr.isTTY === true,
16
+ };
17
+ }
18
+ /** A reporter that says nothing. What `--json` gets, and what a test gets by default. */
19
+ export function silentSink() {
20
+ return { write: () => { }, interactive: false };
21
+ }
22
+ /** How much has to change before a non-interactive sink is told again, as a fraction. */
23
+ const PIPE_STEP = 0.1;
24
+ /**
25
+ * Report progress through one phase.
26
+ *
27
+ * On a terminal the line rewrites in place, to a tenth of a percent — fine enough that a large
28
+ * upload visibly moves rather than appearing to hang. Into a pipe it prints a plain line every ten
29
+ * percent, which is legible in a log and small enough not to bury the result.
30
+ */
31
+ export class Progress {
32
+ sink;
33
+ label;
34
+ lastPrinted = -1;
35
+ dirty = false;
36
+ constructor(sink, label) {
37
+ this.sink = sink;
38
+ this.label = label;
39
+ }
40
+ update(done, total) {
41
+ if (total <= 0)
42
+ return;
43
+ const fraction = Math.min(1, Math.max(0, done / total));
44
+ if (this.sink.interactive) {
45
+ // A tenth of a percent, and never the same number twice: at gigabyte scale the callback
46
+ // fires far more often than the display can change.
47
+ const tenths = Math.floor(fraction * 1000);
48
+ if (tenths === this.lastPrinted)
49
+ return;
50
+ this.lastPrinted = tenths;
51
+ this.dirty = true;
52
+ this.sink.write(`\r ${this.label} ${(tenths / 10).toFixed(1)}% `);
53
+ return;
54
+ }
55
+ const step = Math.floor(fraction / PIPE_STEP);
56
+ if (step === this.lastPrinted)
57
+ return;
58
+ this.lastPrinted = step;
59
+ this.sink.write(` ${this.label} ${Math.round(step * PIPE_STEP * 100)}%\n`);
60
+ }
61
+ /** Finish the line, so whatever prints next starts clean. */
62
+ done() {
63
+ if (this.sink.interactive && this.dirty) {
64
+ this.sink.write(`\r ${this.label} 100.0% \n`);
65
+ this.dirty = false;
66
+ }
67
+ }
68
+ }
69
+ /**
70
+ * A `fetch` that counts the bytes of the request body as they go out.
71
+ *
72
+ * ⛔ THE BODY IS RE-WRAPPED, NOT RE-READ. The bytes are handed to the request as a stream that
73
+ * reports each chunk on its way past, so nothing is copied and the count is what actually left
74
+ * rather than what was queued.
75
+ *
76
+ * ⚠ `duplex: "half"` is required by the fetch specification for a streaming body and Node enforces
77
+ * it. Without it the request throws before a single byte is sent.
78
+ */
79
+ export function countingFetch(onSent, chunkBytes = 256 * 1024) {
80
+ return async (url, init) => {
81
+ const body = init?.body;
82
+ if (!(body instanceof Uint8Array))
83
+ return fetch(url, init);
84
+ const total = body.length;
85
+ let sent = 0;
86
+ const stream = new ReadableStream({
87
+ pull(controller) {
88
+ if (sent >= total) {
89
+ controller.close();
90
+ return;
91
+ }
92
+ const end = Math.min(sent + chunkBytes, total);
93
+ controller.enqueue(body.subarray(sent, end));
94
+ sent = end;
95
+ onSent(sent, total);
96
+ },
97
+ });
98
+ const next = {
99
+ ...init,
100
+ body: stream,
101
+ duplex: "half",
102
+ };
103
+ // ⚠ A streaming body has no length the runtime can work out, and some servers need one.
104
+ const headers = new Headers(init?.headers);
105
+ if (!headers.has("content-length"))
106
+ headers.set("content-length", String(total));
107
+ next.headers = headers;
108
+ return fetch(url, next);
109
+ };
110
+ }
package/dist/prompt.js ADDED
@@ -0,0 +1,145 @@
1
+ // Reading a secret from a terminal without putting it on the screen.
2
+ //
3
+ // ⛔ WHY NOT `readline`. It echoes what is typed, and an account code on screen ends up in
4
+ // scrollback, in a screen share, and in the terminal recording some agents keep. The usual
5
+ // workaround — swapping `process.stdout.write` for a muted one while the question is open — is
6
+ // a global mutation in a process that may be writing other output, and it stays broken if the
7
+ // promise rejects between the swap and the restore. This reads the raw stream instead: nothing
8
+ // global changes, and echo never happens because nothing echoes it.
9
+ //
10
+ // ⚠ THIS REQUIRES A REAL TERMINAL. When stdin is a pipe — which is how an agent usually runs this
11
+ // tool — there is nothing to prompt, and waiting would hang on a stream that will never carry a
12
+ // keystroke. The caller is told to use the environment variable, which is the right answer for
13
+ // an agent anyway.
14
+ //
15
+ // The keystroke rules live in secret-reader.ts and are tested without a terminal.
16
+ import { NmtsError } from "./errors.js";
17
+ import { SecretReader } from "./secret-reader.js";
18
+ /**
19
+ * Bytes typed ahead at a prompt that has already closed, waiting for the next one.
20
+ *
21
+ * ⛔ MODULE STATE, AND IT HAS TO BE. The thing being carried across is one keyboard, and each
22
+ * prompt is a separate call — there is nowhere else for it to live. It holds only what a
23
+ * person typed at this process's own terminal, it is cleared as it is consumed, and nothing
24
+ * reads it but the next prompt.
25
+ */
26
+ let pending = new Uint8Array(0);
27
+ /**
28
+ * Hold the terminal in raw mode across a RUN of prompts.
29
+ *
30
+ * ⛔ WITHOUT THIS, THE GAP BETWEEN TWO PROMPTS ECHOES. `promptSecret` sets raw mode for its own
31
+ * question and restores it afterwards, which was right while this tool asked one thing. `login`
32
+ * now asks three in a row with work in between (the code is checked before a passphrase is
33
+ * asked for), and in that gap the terminal is back in line mode: it echoes what is typed, and
34
+ * its line discipline holds whole lines that the next prompt then never sees. Measured through
35
+ * a pty: a paste of code + passphrase + confirmation ended in "Cancelled" with both
36
+ * passphrases printed on the screen.
37
+ *
38
+ * ⚠ Restoring is what the `finally` is for, including on a throw — a process that exits leaving a
39
+ * terminal in raw mode leaves the person's shell unusable.
40
+ */
41
+ export async function holdTerminal(body) {
42
+ const stdin = process.stdin;
43
+ if (!stdinIsATerminal())
44
+ return body();
45
+ const was = stdin.isRaw === true;
46
+ stdin.setRawMode(true);
47
+ try {
48
+ return await body();
49
+ }
50
+ finally {
51
+ stdin.setRawMode(was);
52
+ // Anything typed past the last question belongs to nobody. It is not carried out of here.
53
+ pending.fill(0);
54
+ pending = new Uint8Array(0);
55
+ }
56
+ }
57
+ export function stdinIsATerminal() {
58
+ return process.stdin.isTTY === true;
59
+ }
60
+ /** Ask for a secret on the terminal, echoing nothing. Ctrl-C and Ctrl-D abandon the prompt. */
61
+ export async function promptSecret(question, envVar) {
62
+ if (!stdinIsATerminal()) {
63
+ throw new NmtsError("There is no terminal to type into (stdin is not a TTY).", {
64
+ exitCode: 3,
65
+ nextStep: `Set ${envVar} in the environment instead.`,
66
+ });
67
+ }
68
+ const stdin = process.stdin;
69
+ const wasRaw = stdin.isRaw === true;
70
+ const reader = new SecretReader();
71
+ // ⛔ TYPED-AHEAD INPUT IS ANSWERED FIRST. A terminal delivers a pasted block as ONE chunk, so
72
+ // somebody answering three prompts with three pasted lines sends all three before the second
73
+ // prompt exists. Reading only from the stream would throw the last two away — and `login`
74
+ // asks three questions in a row, so that was the ordinary case, not the odd one.
75
+ if (pending.length > 0) {
76
+ const carried = pending;
77
+ pending = new Uint8Array(0);
78
+ const ahead = reader.push(carried);
79
+ carried.fill(0);
80
+ if (ahead.kind !== "more") {
81
+ pending = reader.takeLeftover();
82
+ process.stderr.write(`${question}\n`);
83
+ if (ahead.kind === "cancelled") {
84
+ reader.wipe();
85
+ throw new NmtsError("Cancelled.", { exitCode: 130 });
86
+ }
87
+ return ahead.value;
88
+ }
89
+ }
90
+ // ⛔ THE QUESTION GOES TO STDERR, NOT STDOUT. A prompt is not output: stdout is what a caller
91
+ // parses, redirects and — under `nmts mcp` — reads as protocol. An adversarial review caught
92
+ // this one: a passphrase prompt written to stdout put non-protocol bytes on the MCP wire,
93
+ // where the other end sees a parse error and the tool simply disappears.
94
+ process.stderr.write(question);
95
+ stdin.setRawMode(true);
96
+ stdin.resume();
97
+ try {
98
+ return await new Promise((resolve, reject) => {
99
+ const onData = (chunk) => {
100
+ const step = reader.push(chunk);
101
+ if (step.kind === "more")
102
+ return;
103
+ stdin.off("data", onData);
104
+ process.stderr.write("\n");
105
+ if (step.kind === "cancelled") {
106
+ reader.wipe();
107
+ reject(new NmtsError("Cancelled.", { exitCode: 130 }));
108
+ return;
109
+ }
110
+ pending = reader.takeLeftover();
111
+ resolve(step.value);
112
+ };
113
+ stdin.on("data", onData);
114
+ });
115
+ }
116
+ finally {
117
+ reader.wipe();
118
+ stdin.setRawMode(wasRaw);
119
+ stdin.pause();
120
+ }
121
+ }
122
+ /**
123
+ * Ask a question whose answer is not a secret, and echo what is typed.
124
+ *
125
+ * ⛔ NOT FOR SECRETS. `promptSecret` exists for those: it turns echo off and wipes what it read.
126
+ * This one is for a choice, where seeing what you typed is the point.
127
+ *
128
+ * ⛔ IT MUST NOT BE CALLED INSIDE `holdTerminal`. That puts the terminal in raw mode, where
129
+ * readline gets characters one at a time and no line ever arrives.
130
+ *
131
+ * ⚠ Returns the empty string when there is no terminal, so callers can treat "nobody was there"
132
+ * as "nothing was chosen" rather than as an answer. A setup script must not hang here.
133
+ */
134
+ export async function promptLine(question) {
135
+ if (!stdinIsATerminal())
136
+ return "";
137
+ const { createInterface } = await import("node:readline/promises");
138
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
139
+ try {
140
+ return (await rl.question(question)).trim();
141
+ }
142
+ finally {
143
+ rl.close();
144
+ }
145
+ }