@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,176 @@
1
+ // `nmts star`, `nmts pin`, `nmts label` and the three commands that take those marks off again.
2
+ //
3
+ // ⛔ A MARK IS A DESIRED STATE, NEVER A TOGGLE, and that is the whole reason there are six verbs
4
+ // rather than three with a switch. An edit that said "flip it" and was replayed onto a list
5
+ // another device wrote first would flip twice: two people starring one file would end with it
6
+ // unstarred, and neither of them did that. `star` means starred afterwards, whatever it was.
7
+ //
8
+ // ⛔ MARKS BELONG TO FILES. The drive lists starred FILES, counts labelled FILES, and lifts pinned
9
+ // rows to the top of a folder — a folder row carries no mark at all. Putting one on a folder
10
+ // would write a field into the sealed list that no screen anywhere draws, and the only way to
11
+ // ever take it off again would be this tool. So a folder is refused, in words that say why.
12
+ //
13
+ // ⛔ AND NOT ONE OF THEM TELLS THE SERVER ANYTHING. A mark exists only inside the sealed list, so
14
+ // there is no row to change and nothing to undo if the list write fails. What the server does
15
+ // see is a manifest write of a known size at a known moment — the same as a rename.
16
+ //
17
+ // ⛔ ONE WRITE PER RUN, HOWEVER MANY PATHS. The intents these build take a set of ids for exactly
18
+ // that reason. Writing once per path would be one lost compare-and-swap away from half a run.
19
+ import { buildIndex, fullPathOf, KIND_FILE } from "../drive-paths.js";
20
+ import { NmtsError } from "../errors.js";
21
+ import { applyToList, batchTargets } from "../manifest-write.js";
22
+ import { openSession } from "../session.js";
23
+ /** Star one or more files: they show in the drive's favourites as well as in their own folder. */
24
+ export async function star(paths, options = {}) {
25
+ return run("star", paths, undefined, options);
26
+ }
27
+ /** Take the star off. */
28
+ export async function unstar(paths, options = {}) {
29
+ return run("unstar", paths, undefined, options);
30
+ }
31
+ /** Hold one or more files at the top of the folder they are in. */
32
+ export async function pin(paths, options = {}) {
33
+ return run("pin", paths, undefined, options);
34
+ }
35
+ /** Let them fall back into the ordinary order. */
36
+ export async function unpin(paths, options = {}) {
37
+ return run("unpin", paths, undefined, options);
38
+ }
39
+ /**
40
+ * Put one label on one or more files.
41
+ *
42
+ * ⚠ A LABEL EXISTS EXACTLY AS LONG AS SOME FILE WEARS IT. There is no registry to add it to and
43
+ * none to clean up: two devices inventing the same label converge on it instead of colliding,
44
+ * and the last `unlabel` is what makes it stop existing.
45
+ */
46
+ export async function label(name, paths, options = {}) {
47
+ return run("label", paths, name, options);
48
+ }
49
+ /** Take one label off one or more files. */
50
+ export async function unlabel(name, paths, options = {}) {
51
+ return run("unlabel", paths, name, options);
52
+ }
53
+ async function run(verb, paths, name, options) {
54
+ const say = options.write ?? ((line) => process.stdout.write(`${line}\n`));
55
+ const on = !verb.startsWith("un");
56
+ const wantsLabel = verb === "label" || verb === "unlabel";
57
+ // ⚠ Trimmed HERE as well as inside the intent, so the label that is compared against what a
58
+ // file already wears is the same text that gets stored. ` work ` and `work` are one label.
59
+ const text = name === undefined ? "" : name.trim();
60
+ if (wantsLabel && text === "") {
61
+ throw new NmtsError(`\`nmts ${verb}\` needs the label first, then what to put it on.`, {
62
+ exitCode: 2,
63
+ nextStep: `For example: nmts ${verb} work notes.txt report.pdf`,
64
+ });
65
+ }
66
+ if (paths.length === 0) {
67
+ throw new NmtsError(`\`nmts ${verb}\` needs the path of at least one file.`, {
68
+ exitCode: 2,
69
+ nextStep: `\`nmts ls\` prints the paths as this expects them.`,
70
+ });
71
+ }
72
+ const session = await openSession(options);
73
+ const at = Date.now();
74
+ const outcome = { marked: [], already: [] };
75
+ // ⛔ WHICH FILES THESE PATHS NAME IS ASKED INSIDE THE ATTEMPT, not before it. On a lost
76
+ // compare-and-swap the list is read again, and a path is a question about the list: the
77
+ // winner may have renamed something, or moved it, or put it in the trash. Deciding once and
78
+ // re-applying would mark whatever now happens to be sitting at that id.
79
+ const result = await applyToList(session, (now) => {
80
+ const index = buildIndex(now);
81
+ const targets = batchTargets(now, paths, { nothingHappened: "Nothing was marked." });
82
+ const marked = [];
83
+ const already = [];
84
+ for (const target of targets) {
85
+ if (target.kind !== KIND_FILE) {
86
+ throw new NmtsError(`"${fullPathOf(index, target)}" is a folder, and marks are for files.`, {
87
+ exitCode: 4,
88
+ nextStep: `Nothing was marked. A star, a pin and a label are shown on files; a folder that ` +
89
+ `carried one would show it nowhere.`,
90
+ });
91
+ }
92
+ (wears(target, verb, text) === on ? already : marked).push(fullPathOf(index, target));
93
+ }
94
+ outcome.marked = marked;
95
+ outcome.already = already;
96
+ // ⛔ EVERY ID GOES IN, including the ones already like this. The intent is a desired state, so
97
+ // the ones already in it change nothing — and when they are ALL already in it the whole
98
+ // edit is a no-op, which costs no version bump and no download on anybody else's device.
99
+ return intentFor(verb, targets.map((t) => t.id), text, on, at);
100
+ });
101
+ if (options.json) {
102
+ say(JSON.stringify({
103
+ mark: wantsLabel ? "label" : verb === "star" || verb === "unstar" ? "favorite" : "pinned",
104
+ on,
105
+ label: wantsLabel ? text : null,
106
+ marked: outcome.marked,
107
+ already: outcome.already,
108
+ changed: result.changed,
109
+ reappliedAfterConflict: result.reappliedAfterConflict,
110
+ seq: result.seq,
111
+ }));
112
+ return 0;
113
+ }
114
+ // ⛔ THE FILES ARE NAMED, NOT COUNTED, like every other edit in this tool: "2 files starred"
115
+ // cannot disagree with what was written, so it could never catch a run that marked something
116
+ // the caller did not name.
117
+ const names = (list) => list.map((p) => `"${p}"`).join(", ");
118
+ if (outcome.marked.length > 0) {
119
+ say(`${did(verb, text)} ${names(outcome.marked)}.`);
120
+ if (outcome.already.length > 0) {
121
+ say(` ${names(outcome.already)} ${outcome.already.length === 1 ? "was" : "were"} already ${state(verb, text)}.`);
122
+ }
123
+ }
124
+ else {
125
+ say(`Nothing changed: ${names(outcome.already)} ` +
126
+ `${outcome.already.length === 1 ? "is" : "are"} already ${state(verb, text)}.`);
127
+ }
128
+ if (result.changed && result.reappliedAfterConflict) {
129
+ say(` Another device wrote the file list first, so this was applied to that version.`);
130
+ }
131
+ return 0;
132
+ }
133
+ /** The intent one verb writes. `on` is the state asked for, never a flip — see the header. */
134
+ function intentFor(verb, ids, text, on, at) {
135
+ if (verb === "star" || verb === "unstar")
136
+ return { op: "favorite", ids, on, at };
137
+ if (verb === "pin" || verb === "unpin")
138
+ return { op: "pin", ids, on, at };
139
+ return { op: "label", ids, label: text, on, at };
140
+ }
141
+ /** Does this entry already carry the mark the verb is about? */
142
+ function wears(entry, verb, text) {
143
+ if (verb === "star" || verb === "unstar")
144
+ return entry.favorite === true;
145
+ if (verb === "pin" || verb === "unpin")
146
+ return entry.pinned === true;
147
+ return (entry.labels ?? []).includes(text);
148
+ }
149
+ /** What the run did, as the first word of the sentence saying it. */
150
+ function did(verb, text) {
151
+ if (verb === "star")
152
+ return "Starred";
153
+ if (verb === "unstar")
154
+ return "Took the star off";
155
+ if (verb === "pin")
156
+ return "Pinned";
157
+ if (verb === "unpin")
158
+ return "Unpinned";
159
+ if (verb === "label")
160
+ return `Put "${text}" on`;
161
+ return `Took "${text}" off`;
162
+ }
163
+ /** The state the verb asks for, for the sentence about what was already the case. */
164
+ function state(verb, text) {
165
+ if (verb === "star")
166
+ return "starred";
167
+ if (verb === "unstar")
168
+ return "not starred";
169
+ if (verb === "pin")
170
+ return "pinned";
171
+ if (verb === "unpin")
172
+ return "not pinned";
173
+ if (verb === "label")
174
+ return `wearing "${text}"`;
175
+ return `without "${text}"`;
176
+ }
@@ -0,0 +1,139 @@
1
+ // `nmts mcp` — the same account, offered to an agent as tools instead of as a command line.
2
+ //
3
+ // ⛔ WHAT IT WILL AND WILL NOT DO. It offers most of what the command line offers: reading the
4
+ // account, fetching files, uploading them, rearranging them, and sharing one with another
5
+ // account. What it does NOT offer is not a gap to fill in later — a tool a model can call on
6
+ // its own is a different thing from a command a person typed, and this is the list of
7
+ // differences and why each one is there:
8
+ // · Making or revoking a key, signing in or out, and granting this tool's agreements. Those
9
+ // are the person's credentials and the person's consent, and a surface that could grant its
10
+ // own permissions has none.
11
+ // · Passing the human check. A machine cannot; that is what the check is.
12
+ // · Destroying anything permanently — emptying the trash, erasing a file for good. Putting
13
+ // something in the trash IS here, because it can be taken back.
14
+ // · Rebuilding a lost file list. It works, but every name it recovers is a placeholder, and
15
+ // somebody should see that happen rather than read about it afterwards.
16
+ // · Writing the account's disaster-recovery files, and downloading the separate recovery
17
+ // program. Those exist for the day this service is not there, and they are a person's to
18
+ // make and to keep.
19
+ // Nothing here can write outside the one directory the person named when they started it.
20
+ //
21
+ // ⛔ THE OUTPUT DIRECTORY IS CHOSEN BY THE PERSON, NEVER BY THE MODEL. Every tool that writes
22
+ // takes a path INSIDE THE ACCOUNT and lands under that directory. There is nowhere in any tool
23
+ // declaration to put a path on this disk, which is what keeps that true as tools are added: a
24
+ // model that asks for `../../.ssh/authorized_keys` gets a refusal, not a surprise.
25
+ //
26
+ // ⛔ NOTHING BUT PROTOCOL GOES TO STDOUT — and two things had to change for that to be true.
27
+ // Prompts now write to stderr, and this server refuses to prompt at all (`allowPrompt: false`),
28
+ // because its stdin IS the protocol: a passphrase prompt here consumed the client's first
29
+ // message as a guess and put its own question on the wire. Everything a person reads goes to stderr — a stray line
30
+ // on stdout is a parse error at the client and the tools disappear with no explanation.
31
+ import { existsSync, mkdirSync, statSync } from "node:fs";
32
+ import { resolve } from "node:path";
33
+ import { identityOf } from "../account.js";
34
+ import { requireAccountCode } from "../code-access.js";
35
+ import { API_KEY_ENV_VAR, CODE_ENV_VAR, readCredentialsFile, resolveApiKey } from "../credentials.js";
36
+ import { NmtsError } from "../errors.js";
37
+ import { serve } from "../mcp.js";
38
+ // ⚠ Re-exported so callers that knew it here keep working; the rule itself lives one level up now.
39
+ import { destinationFor } from "../safe-path.js";
40
+ export { destinationFor };
41
+ import { BINARY_NAME, PRODUCT_NAME, VERSION } from "../product.js";
42
+ import { resolveNetwork } from "../network.js";
43
+ import { resolveServer } from "../server.js";
44
+ import { fileTools } from "../mcp-tools/files.js";
45
+ import { organiseTools } from "../mcp-tools/organise.js";
46
+ import { readTools } from "../mcp-tools/reads.js";
47
+ import { shareTools } from "../mcp-tools/share.js";
48
+ /**
49
+ * Which account this server is holding, answered without asking anything.
50
+ *
51
+ * ⛔ It is derived here, offline, from the code this process opened at startup. It says nothing
52
+ * about whether that account exists on the server, whether it has credits, or whether the key
53
+ * beside it belongs to the same account — a model that treats "whoami answered" as "I am signed
54
+ * in and can spend" has been misled by its own tool.
55
+ */
56
+ function whoami(ctx) {
57
+ return {
58
+ name: "nmts_whoami",
59
+ description: "Which NMTS account this machine holds the code for. Derived here, offline: it says " +
60
+ "nothing about whether that account exists on the server or has credits.",
61
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
62
+ async run() {
63
+ return [
64
+ `account id ${ctx.accountId}`,
65
+ `server ${ctx.server}`,
66
+ `network ${ctx.network}`,
67
+ `files land ${ctx.outDir}`,
68
+ ].join("\n");
69
+ },
70
+ };
71
+ }
72
+ /**
73
+ * Every schema this program actually serves, for the gate that checks they can be enforced.
74
+ *
75
+ * ⛔ IT BUILDS THE REAL TABLE rather than restating it. A hand-kept list of tool names in a test is
76
+ * a list that goes stale the day somebody adds a tool, and the failure is a tool nobody checks.
77
+ * The context here is a placeholder — no tool reads it while its schema is being looked at.
78
+ */
79
+ export function mcpToolSchemas() {
80
+ const ctx = { server: "https://example.invalid", network: "testnet", outDir: "/", accountId: "-" };
81
+ const all = [whoami(ctx), ...readTools(ctx), ...fileTools(ctx), ...organiseTools(ctx), ...shareTools(ctx)];
82
+ return all.map((t) => ({ name: t.name, inputSchema: t.inputSchema }));
83
+ }
84
+ export async function mcp(options = {}) {
85
+ const note = options.note ?? ((line) => process.stderr.write(`${line}\n`));
86
+ // ⛔ NO PROMPT FROM HERE. stdin is the protocol; see `OpenOptions.allowPrompt`.
87
+ //
88
+ // ⚠ AND THE CODE IS HELD FOR AS LONG AS THIS SERVER RUNS. A passphrase on a sealed store is a
89
+ // gate at startup, not a gate per request: once this process has the code it keeps it, and
90
+ // every tool call it serves uses it without asking again. That is the only shape a server can
91
+ // have — there is nobody to ask mid-session — and it is written down here because somebody
92
+ // deciding whether to leave this running deserves to know it.
93
+ const resolved = await requireAccountCode({ allowPrompt: false });
94
+ const key = resolveApiKey();
95
+ if (key === null) {
96
+ throw new NmtsError("This account has no API key on this machine, and the server needs one.", {
97
+ exitCode: 3,
98
+ nextStep: `Make a key on the account screen at nmts.me and put it in ${API_KEY_ENV_VAR}, or store ` +
99
+ `it with \`${BINARY_NAME} login\`.`,
100
+ });
101
+ }
102
+ const stored = readCredentialsFile();
103
+ const server = resolveServer(options.server ?? stored?.server);
104
+ const network = resolveNetwork(server, options.network ?? stored?.network);
105
+ const identity = await identityOf(resolved.code);
106
+ const outDir = resolve(options.out ?? process.cwd());
107
+ if (!existsSync(outDir))
108
+ mkdirSync(outDir, { recursive: true });
109
+ if (!statSync(outDir).isDirectory()) {
110
+ throw new NmtsError(`${outDir} is not a directory.`, { exitCode: 2 });
111
+ }
112
+ const ctx = { server, network, outDir, accountId: identity.accountId };
113
+ /**
114
+ * The whole surface, in one place.
115
+ *
116
+ * ⛔ THE ORDER IS THE ORDER A CLIENT SHOWS THEM IN, so it goes reads, then fetching, then
117
+ * rearranging, then the one that hands a file to somebody else. A model scanning the list
118
+ * meets the harmless ones first and the irreversible one last, which is the order it should
119
+ * be thinking in.
120
+ */
121
+ const tools = [
122
+ whoami(ctx),
123
+ ...readTools(ctx),
124
+ ...fileTools(ctx),
125
+ ...organiseTools(ctx),
126
+ ...shareTools(ctx),
127
+ ];
128
+ // ⛔ To stderr, deliberately. A person starting this by hand should see what it is; a client
129
+ // reading stdout must see nothing but protocol.
130
+ note(`${PRODUCT_NAME} · ${identity.accountId} · ${network} · files land in ${outDir}`);
131
+ note(`Tools: ${tools.map((t) => t.name).join(" · ")}`);
132
+ await serve({
133
+ input: options.input ?? process.stdin,
134
+ output: options.output ?? ((line) => process.stdout.write(`${line}\n`)),
135
+ tools,
136
+ info: { name: BINARY_NAME, version: VERSION },
137
+ });
138
+ return 0;
139
+ }
@@ -0,0 +1,54 @@
1
+ // `nmts mode` — how much an agent driving this tool may decide without asking.
2
+ //
3
+ // ⛔ IT IS A COMMAND, FOR THE SAME REASON CONSENT IS. A question asked in the middle of another
4
+ // command cannot be answered by a container, a build step, or an agent's subprocess. A command
5
+ // can, its answer is recorded with a date, and it can be looked at afterwards.
6
+ //
7
+ // ⛔ TURNING ONE ON TAKES A FLAG THAT SAYS WHAT IT IS. `nmts mode auto` alone prints what the mode
8
+ // means and stops; adding the flag is the sentence nobody types by accident.
9
+ import { AUTONOMY_MODES, MODE_MEANS, RISK_FLAG, currentMode, setAt, setMode, } from "../autonomy.js";
10
+ import { NmtsError } from "../errors.js";
11
+ import { BINARY_NAME, VERSION } from "../product.js";
12
+ function isMode(value) {
13
+ return AUTONOMY_MODES.includes(value);
14
+ }
15
+ export function mode(wanted, options = {}) {
16
+ const say = options.write ?? ((line) => process.stdout.write(`${line}\n`));
17
+ const now = options.now ?? (() => new Date());
18
+ if (wanted === undefined || wanted === "") {
19
+ const at = currentMode();
20
+ if (options.json === true) {
21
+ say(JSON.stringify({ mode: at, setAt: setAt(), means: MODE_MEANS[at] }));
22
+ return 0;
23
+ }
24
+ say(`${at} — ${MODE_MEANS[at]}`);
25
+ // ⛔ The way to change it is said HERE. Listing what exists without saying how to pick one
26
+ // sends the reader back to the help text and leaves an agent to guess.
27
+ for (const other of AUTONOMY_MODES) {
28
+ if (other !== at)
29
+ say(`${other} — ${MODE_MEANS[other]}`);
30
+ }
31
+ say(`Change it: ${BINARY_NAME} mode <${AUTONOMY_MODES.join("|")}> ${RISK_FLAG}`);
32
+ return 0;
33
+ }
34
+ if (!isMode(wanted)) {
35
+ throw new NmtsError(`There is no mode called "${wanted}".`, {
36
+ exitCode: 2,
37
+ nextStep: `One of: ${AUTONOMY_MODES.join(" · ")}`,
38
+ });
39
+ }
40
+ // ⛔ Turning one OFF never needs the flag. Making the safe direction harder than the risky one
41
+ // is how somebody leaves it on.
42
+ if (wanted !== "off" && options.accepted !== true) {
43
+ throw new NmtsError(MODE_MEANS[wanted], {
44
+ exitCode: 2,
45
+ nextStep: [
46
+ "You bear what an agent does with your files and your credits while this is on.",
47
+ `Turn it on: ${BINARY_NAME} mode ${wanted} ${RISK_FLAG}`,
48
+ ].join("\n "),
49
+ });
50
+ }
51
+ setMode(wanted, VERSION, now());
52
+ say(wanted === "off" ? `off — ${MODE_MEANS.off}` : `${wanted} — ${MODE_MEANS[wanted]}`);
53
+ return 0;
54
+ }
@@ -0,0 +1,45 @@
1
+ // `nmts on-collision` — what this machine does when an upload's name is already in use.
2
+ //
3
+ // ⛔ IT IS A COMMAND FOR THE SAME REASON `mode` IS. The answer is asked for once while somebody is
4
+ // signing in, but a machine that was set up by a script never saw that question, and a person
5
+ // who answered it once has to be able to change it without signing in again.
6
+ //
7
+ // ⛔ NO RISK FLAG HERE, unlike `mode`. This setting does not hand a decision to an agent — it
8
+ // records what a person wants — and the destructive answer is one word that says what it does.
9
+ // What the modes gate is the other case, an agent choosing overwrite for one run with nobody
10
+ // having said so, and that is enforced in `collision.ts` rather than by a flag on this command.
11
+ import { COLLISION_CHOICES, COLLISION_MEANS, currentChoice, setChoice, } from "../collision.js";
12
+ import { NmtsError } from "../errors.js";
13
+ import { BINARY_NAME, VERSION } from "../product.js";
14
+ function isChoice(value) {
15
+ return COLLISION_CHOICES.includes(value);
16
+ }
17
+ export function onCollision(wanted, options = {}) {
18
+ const say = options.write ?? ((line) => process.stdout.write(`${line}\n`));
19
+ const now = options.now ?? (() => new Date());
20
+ if (wanted === undefined || wanted === "") {
21
+ const at = currentChoice();
22
+ if (options.json === true) {
23
+ say(JSON.stringify({ onCollision: at, means: COLLISION_MEANS[at] }));
24
+ return 0;
25
+ }
26
+ say(`${at} — ${COLLISION_MEANS[at]}`);
27
+ // The other one, and how to pick it. Listing what exists without saying how to choose sends
28
+ // the reader back to the help text.
29
+ for (const other of COLLISION_CHOICES) {
30
+ if (other !== at)
31
+ say(`${other} — ${COLLISION_MEANS[other]}`);
32
+ }
33
+ say(`Change it: ${BINARY_NAME} on-collision <${COLLISION_CHOICES.join("|")}>`);
34
+ return 0;
35
+ }
36
+ if (!isChoice(wanted)) {
37
+ throw new NmtsError(`There is no such answer: "${wanted}".`, {
38
+ exitCode: 2,
39
+ nextStep: `One of: ${COLLISION_CHOICES.join(" · ")}`,
40
+ });
41
+ }
42
+ setChoice(wanted, VERSION, now());
43
+ say(`${wanted} — ${COLLISION_MEANS[wanted]}`);
44
+ return 0;
45
+ }