@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,50 @@
1
+ // What every tool in the agent surface is handed, and the two helpers they all use.
2
+ //
3
+ // ⛔ THE PLACE IS THE PERSON'S, THE NAMES ARE THE MODEL'S. `outDir` was resolved once, from what
4
+ // the person typed when they started the server, and no tool takes a destination argument. A
5
+ // model chooses which file, never where it lands. That is the one rule that makes the rest of
6
+ // this surface safe to widen: adding a tool cannot add a way out of that directory, because
7
+ // there is nowhere in a tool declaration to put one.
8
+ /** The server and network every command takes, in the shape they take it. */
9
+ export function common(ctx) {
10
+ return { server: ctx.server, network: ctx.network };
11
+ }
12
+ /**
13
+ * Collect what a command would have printed, so it can be handed to a model instead.
14
+ *
15
+ * ⛔ EVERY TOOL USES THIS AND NONE OF THEM PRINT. On this server stdout is the protocol wire; a
16
+ * stray line there is a parse error at the client and the whole tool list disappears with no
17
+ * explanation. Commands write through an injected sink precisely so that this can be true.
18
+ */
19
+ export function collector() {
20
+ const lines = [];
21
+ return { lines, write: (line) => lines.push(line) };
22
+ }
23
+ /**
24
+ * Run one command with its output collected, and hand back what it wrote.
25
+ *
26
+ * ⚠ A non-zero exit code is NOT turned into a throw. A command that returns 4 has already written
27
+ * the explanation the model needs, and replacing it with a generic failure would throw that
28
+ * away. The commands that genuinely cannot proceed throw, and the transport reports those.
29
+ */
30
+ export async function say(run) {
31
+ const out = collector();
32
+ await run(out.write);
33
+ return out.lines.join("\n");
34
+ }
35
+ /** A required string argument, with the same refusal every tool gives for it. */
36
+ export function needString(args, name) {
37
+ const value = args[name];
38
+ // The transport already checked the declared schema, so this only catches an empty string —
39
+ // which is a legal string and never a legal path, id or address.
40
+ if (typeof value !== "string" || value === "")
41
+ throw new Error(`\`${name}\` is required.`);
42
+ return value;
43
+ }
44
+ /** Several paths at once, as the multi-path commands take them. */
45
+ export function needPaths(args) {
46
+ const value = args["paths"];
47
+ if (!Array.isArray(value) || value.length === 0)
48
+ throw new Error("`paths` must hold at least one path.");
49
+ return value.filter((p) => typeof p === "string" && p !== "");
50
+ }
@@ -0,0 +1,133 @@
1
+ // Fetching files out of the account, and putting files into it.
2
+ //
3
+ // ⛔ TWO OF THESE SPEND AND THEIR DESCRIPTIONS SAY SO FIRST. A model reads the description before
4
+ // it decides; burying the cost after the parameters is how a tool gets called to "see what
5
+ // happens". Both take a dry_run that prices without spending, and the transport now refuses a
6
+ // dry_run that is not a real boolean — before that check, `"true"` read as false and the paid
7
+ // branch ran.
8
+ //
9
+ // ⛔ EVERY DESTINATION IS DERIVED FROM THE DIRECTORY THE PERSON CHOSE. No tool here takes a path on
10
+ // this disk. `nmts_get` keeps only the last segment of the account path; `nmts_pull` builds its
11
+ // tree under that directory and refuses a stored name that would climb out; `nmts_receive` uses
12
+ // the sender's name the same way. The only path a model may choose is one INSIDE the account.
13
+ import { get } from "../commands/get.js";
14
+ import { pull } from "../commands/pull.js";
15
+ import { push } from "../commands/push.js";
16
+ import { put } from "../commands/put.js";
17
+ import { receive } from "../commands/receive.js";
18
+ import { destinationFor } from "../safe-path.js";
19
+ import { common, needString, say } from "./context.js";
20
+ const SPENDS = "⛔ THIS SPENDS THE ACCOUNT'S CREDITS — one credit per started mebibyte, per lease period, and " +
21
+ "credits are not refundable. Pass dry_run to be told the price without spending it. The first " +
22
+ "upload on a machine stops and asks the person to agree; show them what it says rather than " +
23
+ "agreeing for them.";
24
+ export function fileTools(ctx) {
25
+ return [
26
+ {
27
+ name: "nmts_get",
28
+ description: `Fetch one file from the NMTS account, decrypt it, and write it into ${ctx.outDir}. Takes ` +
29
+ `a path exactly as nmts_list prints it. It leaves nothing at that name rather than ` +
30
+ `writing a wrong or partial file, and it will not replace a file that is already there. ` +
31
+ `The reply says where it went.`,
32
+ inputSchema: {
33
+ type: "object",
34
+ properties: { path: { type: "string", description: "The file's path inside the account." } },
35
+ required: ["path"],
36
+ additionalProperties: false,
37
+ },
38
+ run: (args) => {
39
+ const wanted = needString(args, "path");
40
+ return say((write) => get(wanted, { ...common(ctx), out: destinationFor(ctx.outDir, wanted), json: true, write }));
41
+ },
42
+ },
43
+ {
44
+ name: "nmts_pull",
45
+ description: `Fetch a whole folder — or the whole account, if no folder is named — and rebuild its ` +
46
+ `shape under ${ctx.outDir}. Unlike fetching one file it carries on past a file that will ` +
47
+ `not come back, because getting nineteen of twenty is better than getting none; the reply ` +
48
+ `lists what was written, what was skipped as already there, and what failed and why.`,
49
+ inputSchema: {
50
+ type: "object",
51
+ properties: {
52
+ folder: {
53
+ type: "string",
54
+ description: "A folder path inside the account. Omit for everything.",
55
+ },
56
+ },
57
+ additionalProperties: false,
58
+ },
59
+ run: (args) => say((write) => pull(typeof args["folder"] === "string" ? args["folder"] : undefined, {
60
+ ...common(ctx),
61
+ out: ctx.outDir,
62
+ json: true,
63
+ write,
64
+ })),
65
+ },
66
+ {
67
+ name: "nmts_receive",
68
+ description: `Fetch a file another account shared with this one, using an id from nmts_shares, and ` +
69
+ `write it into ${ctx.outDir} under the name the sender gave it. The sender's identity is ` +
70
+ `checked before anything is opened.`,
71
+ inputSchema: {
72
+ type: "object",
73
+ properties: { id: { type: "string", description: "The share id, from nmts_shares." } },
74
+ required: ["id"],
75
+ additionalProperties: false,
76
+ },
77
+ run: (args) => say((write) => receive(needString(args, "id"), { ...common(ctx), intoDir: ctx.outDir, json: true, write })),
78
+ },
79
+ {
80
+ name: "nmts_put",
81
+ description: `Encrypt one file from this machine and upload it to the NMTS account. ${SPENDS} A name ` +
82
+ `already taken in the destination is numbered rather than replacing what is there.`,
83
+ inputSchema: {
84
+ type: "object",
85
+ properties: {
86
+ file: { type: "string", description: "Path to a file ON THIS MACHINE to upload." },
87
+ name: { type: "string", description: "The name it gets in the account. Defaults to the file's own." },
88
+ to: { type: "string", description: "An existing folder in the account, as nmts_list prints it." },
89
+ dry_run: { type: "boolean", description: "Say what it would cost and stop. Nothing is sent or charged." },
90
+ },
91
+ required: ["file"],
92
+ additionalProperties: false,
93
+ },
94
+ run: (args) => say((write) => put(needString(args, "file"), {
95
+ ...common(ctx),
96
+ json: true,
97
+ ...(typeof args["name"] === "string" ? { name: args["name"] } : {}),
98
+ ...(typeof args["to"] === "string" ? { to: args["to"] } : {}),
99
+ ...(args["dry_run"] === true ? { dryRun: true } : {}),
100
+ write,
101
+ })),
102
+ },
103
+ {
104
+ name: "nmts_push",
105
+ description: `Upload a whole directory from this machine, rebuilding its shape in the account. ` +
106
+ `${SPENDS} ⛔ It stops at the FIRST failure rather than carrying on — the opposite of ` +
107
+ `nmts_pull, and on purpose: a failure that really means the account cannot pay any more ` +
108
+ `would, if carried past, ask to pay for every remaining file. Files already in the ` +
109
+ `destination are skipped, so running it again is safe and sends only what is missing. ` +
110
+ `Names beginning with a dot are left alone unless include_hidden is true — a directory of ` +
111
+ `source code keeps its credentials in exactly those files.`,
112
+ inputSchema: {
113
+ type: "object",
114
+ properties: {
115
+ directory: { type: "string", description: "Path to a directory ON THIS MACHINE." },
116
+ to: { type: "string", description: "An existing folder in the account to put it under." },
117
+ include_hidden: { type: "boolean", description: "Also send names beginning with a dot." },
118
+ dry_run: { type: "boolean", description: "Price the whole tree and stop. Nothing is sent or charged." },
119
+ },
120
+ required: ["directory"],
121
+ additionalProperties: false,
122
+ },
123
+ run: (args) => say((write) => push(needString(args, "directory"), {
124
+ ...common(ctx),
125
+ json: true,
126
+ ...(typeof args["to"] === "string" ? { to: args["to"] } : {}),
127
+ ...(args["include_hidden"] === true ? { hidden: true } : {}),
128
+ ...(args["dry_run"] === true ? { dryRun: true } : {}),
129
+ write,
130
+ })),
131
+ },
132
+ ];
133
+ }
@@ -0,0 +1,128 @@
1
+ // Rearranging what the account holds, without moving a byte or spending anything.
2
+ //
3
+ // ⛔ EVERYTHING HERE IS AN EDIT TO THE SEALED LIST AND NOTHING HERE IS PERMANENT. Names, folders
4
+ // and marks live only inside the list the account code opens; the server never sees them and no
5
+ // stored bytes move. That is why these are the tools a model may use freely: the worst outcome
6
+ // is a tidy-up somebody has to undo, and every one of them has an undo.
7
+ //
8
+ // ⛔ WHAT IS DELIBERATELY ABSENT: permanent erase, and sweeping the trash. Putting something in the
9
+ // trash is reversible and is here; taking it out for good is not, and a machine does not get to
10
+ // make that call on somebody's files. The command line still has both for a person.
11
+ //
12
+ // ⛔ SIX MARK VERBS ARE ONE TOOL, NOT SIX. A model choosing between `nmts_star` and `nmts_unstar`
13
+ // is choosing between two spellings of the same decision, and the pair that gets forgotten is
14
+ // the "un" one. One tool with the mark named and a switch says what is actually being decided.
15
+ import { label, pin, star, unlabel, unpin, unstar } from "../commands/marks.js";
16
+ import { mkdir, mv, rename } from "../commands/organise.js";
17
+ import { restore, rm } from "../commands/trash.js";
18
+ import { common, needPaths, needString, say } from "./context.js";
19
+ export function organiseTools(ctx) {
20
+ return [
21
+ {
22
+ name: "nmts_mkdir",
23
+ description: "Make a folder in the NMTS account, including any parent folders that are missing. " +
24
+ "Folders exist only inside the account's sealed list; making one costs nothing.",
25
+ inputSchema: {
26
+ type: "object",
27
+ properties: { path: { type: "string", description: "The folder path to make." } },
28
+ required: ["path"],
29
+ additionalProperties: false,
30
+ },
31
+ run: (args) => say((write) => mkdir(needString(args, "path"), { ...common(ctx), json: true, write })),
32
+ },
33
+ {
34
+ name: "nmts_move",
35
+ description: "Move one or more entries into a folder in the NMTS account. Nothing is re-uploaded and " +
36
+ "nothing is charged — only the sealed list changes. A name already taken in the " +
37
+ "destination is numbered rather than replacing what is there.",
38
+ inputSchema: {
39
+ type: "object",
40
+ properties: {
41
+ paths: {
42
+ type: "array",
43
+ items: { type: "string" },
44
+ description: "The entries to move, as nmts_list prints them.",
45
+ },
46
+ to: { type: "string", description: "The destination folder." },
47
+ },
48
+ required: ["paths", "to"],
49
+ additionalProperties: false,
50
+ },
51
+ run: (args) => say((write) => mv([...needPaths(args), needString(args, "to")], { ...common(ctx), json: true, write })),
52
+ },
53
+ {
54
+ name: "nmts_rename",
55
+ description: "Rename one entry in the NMTS account. The name lives inside the sealed list, so this " +
56
+ "moves no bytes and costs nothing.",
57
+ inputSchema: {
58
+ type: "object",
59
+ properties: {
60
+ path: { type: "string", description: "The entry to rename." },
61
+ name: { type: "string", description: "Its new name. A name, not a path." },
62
+ },
63
+ required: ["path", "name"],
64
+ additionalProperties: false,
65
+ },
66
+ run: (args) => say((write) => rename(needString(args, "path"), needString(args, "name"), { ...common(ctx), json: true, write })),
67
+ },
68
+ {
69
+ name: "nmts_mark",
70
+ description: "Put a mark on files, or take one off. `star` is the account's favourites, `pin` keeps a " +
71
+ "file at the top of its folder, and `label` attaches a word you choose (give it in " +
72
+ "`name`). Marks live inside the sealed list and are refused on folders.",
73
+ inputSchema: {
74
+ type: "object",
75
+ properties: {
76
+ paths: { type: "array", items: { type: "string" }, description: "The files to mark." },
77
+ mark: { type: "string", enum: ["star", "pin", "label"], description: "Which mark." },
78
+ on: { type: "boolean", description: "true puts the mark on, false takes it off. Required." },
79
+ name: { type: "string", description: "The label's word. Required when mark is label." },
80
+ },
81
+ required: ["paths", "mark", "on"],
82
+ additionalProperties: false,
83
+ },
84
+ run: (args) => {
85
+ const paths = needPaths(args);
86
+ const mark = needString(args, "mark");
87
+ const on = args["on"];
88
+ if (typeof on !== "boolean")
89
+ throw new Error("`on` is required: true to mark, false to unmark.");
90
+ const options = { ...common(ctx), json: true };
91
+ return say((write) => {
92
+ const opts = { ...options, write };
93
+ if (mark === "star")
94
+ return on ? star(paths, opts) : unstar(paths, opts);
95
+ if (mark === "pin")
96
+ return on ? pin(paths, opts) : unpin(paths, opts);
97
+ const name = needString(args, "name");
98
+ return on ? label(name, paths, opts) : unlabel(name, paths, opts);
99
+ });
100
+ },
101
+ },
102
+ {
103
+ name: "nmts_trash",
104
+ description: "Put files in the NMTS account's trash. Nothing is destroyed and nothing is refunded: the " +
105
+ "storage stays bought and the file can be brought back with nmts_restore. Emptying the " +
106
+ "trash for good is not something this surface can do — that is a person's decision at the " +
107
+ "command line.",
108
+ inputSchema: {
109
+ type: "object",
110
+ properties: { paths: { type: "array", items: { type: "string" }, description: "The files to trash." } },
111
+ required: ["paths"],
112
+ additionalProperties: false,
113
+ },
114
+ run: (args) => say((write) => rm(needPaths(args), { ...common(ctx), json: true, write })),
115
+ },
116
+ {
117
+ name: "nmts_restore",
118
+ description: "Take files back out of the NMTS account's trash, into the folder they came from.",
119
+ inputSchema: {
120
+ type: "object",
121
+ properties: { paths: { type: "array", items: { type: "string" }, description: "The files to restore." } },
122
+ required: ["paths"],
123
+ additionalProperties: false,
124
+ },
125
+ run: (args) => say((write) => restore(needPaths(args), { ...common(ctx), json: true, write })),
126
+ },
127
+ ];
128
+ }
@@ -0,0 +1,89 @@
1
+ // The tools that only look: what the account holds, what it is spending, what is about to expire.
2
+ //
3
+ // ⛔ NONE OF THESE COST ANYTHING AND NONE OF THEM WRITE. That is worth stating rather than
4
+ // assuming, because it is what makes them safe to call in a loop — an agent working out what to
5
+ // do next will call them repeatedly, and a surface where "just checking" costs money teaches
6
+ // the opposite of the habit this tool wants.
7
+ import { publicCode } from "../commands/public-code.js";
8
+ import { balance } from "../commands/balance.js";
9
+ import { expiring } from "../commands/expiring.js";
10
+ import { ls } from "../commands/ls.js";
11
+ import { shares } from "../commands/share.js";
12
+ import { usage } from "../commands/usage.js";
13
+ import { common, say } from "./context.js";
14
+ const NO_ARGS = { type: "object", properties: {}, additionalProperties: false };
15
+ export function readTools(ctx) {
16
+ return [
17
+ {
18
+ name: "nmts_list",
19
+ description: "List the files stored in the NMTS account, as JSON. Paths are what nmts_get takes. " +
20
+ "Entries in the trash are left out unless include_trashed is true, and the reply says " +
21
+ "how many were left out. Optionally narrow with a search word and choose an order.",
22
+ inputSchema: {
23
+ type: "object",
24
+ properties: {
25
+ include_trashed: { type: "boolean", description: "Include what is in the trash." },
26
+ find: { type: "string", description: "Keep only entries whose name contains this." },
27
+ sort: {
28
+ type: "string",
29
+ enum: ["name", "size", "date"],
30
+ description: "Order the listing. Absent means the path order this tool has always printed.",
31
+ },
32
+ descending: { type: "boolean", description: "Reverse the order." },
33
+ },
34
+ additionalProperties: false,
35
+ },
36
+ run: (args) => say((write) => ls({
37
+ ...common(ctx),
38
+ json: true,
39
+ all: args["include_trashed"] === true,
40
+ ...(typeof args["find"] === "string" ? { find: args["find"] } : {}),
41
+ ...(typeof args["sort"] === "string" ? { sort: args["sort"] } : {}),
42
+ ...(args["descending"] === true ? { desc: true } : {}),
43
+ write,
44
+ })),
45
+ },
46
+ {
47
+ name: "nmts_usage",
48
+ description: "How much this account holds: how many files and folders, how many bytes, how much is in " +
49
+ "the trash, and the largest few. Reads the account's own sealed list — it costs nothing " +
50
+ "and says nothing about credits.",
51
+ inputSchema: NO_ARGS,
52
+ run: () => say((write) => usage({ ...common(ctx), json: true, write })),
53
+ },
54
+ {
55
+ name: "nmts_expiring",
56
+ description: "Files whose bought storage runs out soon, with how long each has left. Storage on NMTS " +
57
+ "is a lease, not a purchase: a file whose lease ends is gone. Reads the storage " +
58
+ "network's own clock, so it refuses rather than guessing when it cannot reach it.",
59
+ inputSchema: NO_ARGS,
60
+ run: () => say((write) => expiring({ ...common(ctx), json: true, write })),
61
+ },
62
+ {
63
+ name: "nmts_balance",
64
+ description: "How many credits this account has left, what that buys, and the ceilings on spending. " +
65
+ "Read this before uploading anything large: the price of an upload is printed, but only " +
66
+ "this says whether the account can pay it.",
67
+ inputSchema: NO_ARGS,
68
+ run: () => say((write) => balance({ ...common(ctx), json: true, write })),
69
+ },
70
+ {
71
+ name: "nmts_public_code",
72
+ description: "The account's PUBLIC CODE — the value other accounts send files to — and whether it has " +
73
+ "been published yet. Until it is published nobody can send to this account. ⛔ It only " +
74
+ "reads. Publishing is permanent and is a person's decision at the command line: if the " +
75
+ "reply says it is not published, say so rather than working around it. ⚠ Not the account " +
76
+ "code, which opens every file and is never given away.",
77
+ inputSchema: NO_ARGS,
78
+ run: () => say((write) => publicCode({ ...common(ctx), json: true, write })),
79
+ },
80
+ {
81
+ name: "nmts_shares",
82
+ description: "Files this account has shared with somebody, and files somebody has shared with it. The " +
83
+ "ids in the received list are what nmts_receive takes; the ids in the sent list are what " +
84
+ "nmts_unshare takes.",
85
+ inputSchema: NO_ARGS,
86
+ run: () => say((write) => shares({ ...common(ctx), json: true, write })),
87
+ },
88
+ ];
89
+ }
@@ -0,0 +1,61 @@
1
+ // Handing a file to another account, and taking it back.
2
+ //
3
+ // ⛔ THE UNDO DOES NOT UNDO IT, AND THE DESCRIPTION SAYS SO. Withdrawing a share stops further
4
+ // downloads and reaches nothing already fetched. That is not a flaw waiting to be fixed — it is
5
+ // what handing somebody a file means — so it is stated before the first share rather than after.
6
+ //
7
+ // ⛔ A PERSON HAS TO HAVE AGREED, AND THIS SURFACE CANNOT AGREE FOR THEM. Sharing is behind a
8
+ // once-per-machine agreement in this tool's own record. If it has not been given, the first
9
+ // call here stops and returns what the agreement says. ⚠ Show that to the person; do not run
10
+ // the command that grants it on their behalf. Nothing in a command-line tool can tell whether a
11
+ // person or a program typed a grant, so this is a rule rather than a mechanism, and saying
12
+ // otherwise would be claiming a protection that is not there.
13
+ //
14
+ // ⛔ THE RECIPIENT'S PUBLIC CODE IS NOT CHECKED AGAINST A PERSON. A share sent to a well-formed
15
+ // code that belongs to somebody else is sent, and is not recallable. Confirm it with whoever
16
+ // gave it to you, out of band, before calling this.
17
+ import { share, unshare } from "../commands/share.js";
18
+ import { common, needString, say } from "./context.js";
19
+ export function shareTools(ctx) {
20
+ return [
21
+ {
22
+ name: "nmts_share",
23
+ description: "Give another NMTS account the key to one file in this account. ⛔ IT CANNOT BE TAKEN " +
24
+ "BACK: withdrawing the share stops further downloads and cannot reach a copy the " +
25
+ "recipient already fetched, and a public code typed wrongly is a share sent to whoever " +
26
+ "holds that code. The first share on a machine stops and asks the person to agree — show " +
27
+ "them what it says rather than agreeing for them. Nothing is uploaded and nothing is " +
28
+ "charged; the recipient pays nothing either.",
29
+ inputSchema: {
30
+ type: "object",
31
+ properties: {
32
+ path: { type: "string", description: "The file to share, as nmts_list prints it." },
33
+ public_code: {
34
+ type: "string",
35
+ description: "The recipient's PUBLIC CODE, given to you by them. Not their account code.",
36
+ },
37
+ },
38
+ required: ["path", "public_code"],
39
+ additionalProperties: false,
40
+ },
41
+ run: (args) => say((write) => share(needString(args, "path"), needString(args, "public_code"), {
42
+ ...common(ctx),
43
+ json: true,
44
+ write,
45
+ })),
46
+ },
47
+ {
48
+ name: "nmts_unshare",
49
+ description: "Withdraw a share this account made, using an id from nmts_shares. It stops any further " +
50
+ "download and does NOT reach a copy already fetched. Safe to call: taking something back " +
51
+ "is the direction this door is meant to fail in.",
52
+ inputSchema: {
53
+ type: "object",
54
+ properties: { id: { type: "string", description: "The share id, from nmts_shares." } },
55
+ required: ["id"],
56
+ additionalProperties: false,
57
+ },
58
+ run: (args) => say((write) => unshare(needString(args, "id"), { ...common(ctx), json: true, write })),
59
+ },
60
+ ];
61
+ }
package/dist/mcp.js ADDED
@@ -0,0 +1,142 @@
1
+ // The Model Context Protocol, spoken over stdin/stdout, with no library.
2
+ //
3
+ // ⛔ WHY NO SDK. This package promises that a separate licence can be arranged, and that promise is
4
+ // only true while every line of it is ours to license. Each dependency is a copy of somebody
5
+ // else's copyright riding along — a check enforces that they are all permissive, but the surface
6
+ // here is small enough that not adding one is simply better. MCP over stdio is JSON-RPC 2.0 in
7
+ // newline-delimited JSON; that is the whole transport.
8
+ //
9
+ // ⛔ STDOUT IS THE WIRE. Anything printed there that is not a protocol message corrupts the session
10
+ // — the client sees a parse error and the tools vanish. So nothing in this file writes to stdout
11
+ // except `send`, and everything a person should read goes to stderr. A test holds that line.
12
+ //
13
+ // ⚠ WHAT THIS IS NOT: it is not a sandbox. A tool call here can read the account's file list and
14
+ // write a file into the directory the person chose when they started the server. It cannot reach
15
+ // anywhere else on disk, and it cannot make or revoke a key — those need a person at a browser.
16
+ import { createInterface } from "node:readline";
17
+ import { checkArgs } from "./mcp-args.js";
18
+ import { renderError } from "./errors.js";
19
+ /**
20
+ * Protocol versions this server knows how to speak.
21
+ *
22
+ * ⛔ Ordered newest first. If the client asks for one of these it gets that one back; if it asks
23
+ * for anything else it gets the newest we know, which is what the specification says to do —
24
+ * guessing that an unknown version is compatible is how a session half-works.
25
+ */
26
+ export const PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
27
+ const PARSE_ERROR = -32700;
28
+ const INVALID_REQUEST = -32600;
29
+ const METHOD_NOT_FOUND = -32601;
30
+ const INTERNAL_ERROR = -32603;
31
+ function isRequest(value) {
32
+ if (typeof value !== "object" || value === null)
33
+ return false;
34
+ const v = value;
35
+ return v["jsonrpc"] === "2.0" && typeof v["method"] === "string";
36
+ }
37
+ /**
38
+ * Answer one request. Returns the response object, or `null` for a notification.
39
+ *
40
+ * Pure apart from the tools it is handed, so the whole protocol is testable without pipes.
41
+ */
42
+ export async function handle(request, tools, info) {
43
+ if (!isRequest(request)) {
44
+ return { jsonrpc: "2.0", id: null, error: { code: INVALID_REQUEST, message: "not a JSON-RPC 2.0 request" } };
45
+ }
46
+ const id = request.id;
47
+ const reply = (result) => ({ jsonrpc: "2.0", id, result });
48
+ const fail = (code, message) => ({ jsonrpc: "2.0", id, error: { code, message } });
49
+ switch (request.method) {
50
+ case "initialize": {
51
+ const asked = request.params?.["protocolVersion"];
52
+ const version = typeof asked === "string" && PROTOCOL_VERSIONS.includes(asked)
53
+ ? asked
54
+ : PROTOCOL_VERSIONS[0];
55
+ return reply({ protocolVersion: version, capabilities: { tools: {} }, serverInfo: info });
56
+ }
57
+ // Notifications carry no id and get no answer. Returning one would be a protocol error.
58
+ case "notifications/initialized":
59
+ case "notifications/cancelled":
60
+ return null;
61
+ case "ping":
62
+ return reply({});
63
+ case "tools/list":
64
+ return reply({
65
+ tools: tools.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })),
66
+ });
67
+ case "tools/call": {
68
+ const name = request.params?.["name"];
69
+ const tool = tools.find((t) => t.name === name);
70
+ if (tool === undefined)
71
+ return fail(METHOD_NOT_FOUND, `no tool named ${String(name)}`);
72
+ // ⛔ THE SCHEMA IS CHECKED HERE, ONCE, FOR EVERY TOOL. It used to be advertised and never
73
+ // enforced: anything that was not an object became `{}` and everything else went straight
74
+ // through, so a tool declaring `dry_run: boolean` was handed the STRING "true" and its
75
+ // `=== true` test read it as false — a request for a price became a paid upload. Checking
76
+ // in each tool would be the same four checks written twenty times, and the twentieth would
77
+ // forget. ⛔ A wrong argument is REFUSED, never repaired: guessing what `"true"` meant is
78
+ // deciding on the caller's behalf which branch spends money.
79
+ const raw = request.params?.["arguments"];
80
+ const args = raw === undefined ? {} : raw;
81
+ const problems = checkArgs(tool.inputSchema, args);
82
+ if (problems.length > 0) {
83
+ return reply({
84
+ content: [{ type: "text", text: `${tool.name}: ${problems.join("; ")}` }],
85
+ isError: true,
86
+ });
87
+ }
88
+ try {
89
+ // Narrowed by the check above: `checkArgs` refuses anything that is not an object.
90
+ const checked = typeof args === "object" && args !== null && !Array.isArray(args) ? { ...args } : {};
91
+ return reply({ content: [{ type: "text", text: await tool.run(checked) }] });
92
+ }
93
+ catch (error) {
94
+ // ⛔ A failed TOOL is not a failed SESSION. The model is told what went wrong and can try
95
+ // something else; a JSON-RPC error would look to some clients like the server broke.
96
+ //
97
+ // ⛔ AND IT IS TOLD WHAT TO DO NEXT. This used to send `error.message` alone, which threw
98
+ // away the one line the refusal carries for exactly this reader — the model here has no
99
+ // terminal to look at and no other source. Forty of the server's fifty-six refusals
100
+ // carry that line, and none of them reached this path until 2026-08-30. `renderError`
101
+ // is the same shaping the terminal gets, so the two cannot drift apart.
102
+ return reply({
103
+ content: [{ type: "text", text: renderError(error, "nmts") }],
104
+ isError: true,
105
+ });
106
+ }
107
+ }
108
+ default:
109
+ // ⛔ An unknown NOTIFICATION is silence, not an error: a client is allowed to send ones we
110
+ // have never heard of, and answering would put an unasked-for message on the wire.
111
+ if (id === undefined)
112
+ return null;
113
+ return fail(METHOD_NOT_FOUND, `unknown method ${request.method}`);
114
+ }
115
+ }
116
+ /** Read newline-delimited JSON-RPC from `input` until it ends, answering on `output`. */
117
+ export async function serve(options) {
118
+ const lines = createInterface({ input: options.input, crlfDelay: Infinity });
119
+ for await (const line of lines) {
120
+ if (line.trim() === "")
121
+ continue;
122
+ let parsed;
123
+ try {
124
+ parsed = JSON.parse(line);
125
+ }
126
+ catch {
127
+ options.output(JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: PARSE_ERROR, message: "invalid JSON" } }));
128
+ continue;
129
+ }
130
+ let response;
131
+ try {
132
+ response = await handle(parsed, options.tools, options.info);
133
+ }
134
+ catch (error) {
135
+ // The handler itself falling over must not end the session either.
136
+ const message = error instanceof Error ? error.message : "the server failed";
137
+ response = { jsonrpc: "2.0", id: null, error: { code: INTERNAL_ERROR, message } };
138
+ }
139
+ if (response !== null)
140
+ options.output(JSON.stringify(response));
141
+ }
142
+ }