@3minapi/cli 0.0.0-stage → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +82 -2
  3. package/dist/bin.js +54 -0
  4. package/dist/cli.js +241 -0
  5. package/dist/commands/auth.js +573 -0
  6. package/dist/commands/common.js +48 -0
  7. package/dist/commands/dashboard.js +298 -0
  8. package/dist/commands/delete.js +167 -0
  9. package/dist/commands/doctor.js +326 -0
  10. package/dist/commands/guide.js +84 -0
  11. package/dist/commands/index.js +28 -0
  12. package/dist/commands/init.js +248 -0
  13. package/dist/commands/logs.js +154 -0
  14. package/dist/commands/new.js +109 -0
  15. package/dist/commands/paths.js +64 -0
  16. package/dist/commands/reload.js +32 -0
  17. package/dist/commands/results.js +445 -0
  18. package/dist/commands/start.js +443 -0
  19. package/dist/commands/stop.js +68 -0
  20. package/dist/commands/tasks.js +93 -0
  21. package/dist/commands/worker.js +178 -0
  22. package/dist/core/api.js +132 -0
  23. package/dist/core/config.js +188 -0
  24. package/dist/core/control-db.js +462 -0
  25. package/dist/core/credentials.js +75 -0
  26. package/dist/core/domain/constants.js +121 -0
  27. package/dist/core/domain/contract.js +102 -0
  28. package/dist/core/domain/errors.js +57 -0
  29. package/dist/core/domain/index.js +3 -0
  30. package/dist/core/domain/types.js +1 -0
  31. package/dist/core/error-report.js +103 -0
  32. package/dist/core/host/detach.js +49 -0
  33. package/dist/core/host/live-connection.js +270 -0
  34. package/dist/core/host/live-receiver.js +264 -0
  35. package/dist/core/host/module.js +1 -0
  36. package/dist/core/host/poller.js +113 -0
  37. package/dist/core/host/pull-step.js +54 -0
  38. package/dist/core/host/receiver.js +1 -0
  39. package/dist/core/host/reload.js +34 -0
  40. package/dist/core/host/runner-lock.js +245 -0
  41. package/dist/core/host/session.js +84 -0
  42. package/dist/core/host/usage.js +20 -0
  43. package/dist/core/log-read.js +59 -0
  44. package/dist/core/logger.js +206 -0
  45. package/dist/core/names.js +27 -0
  46. package/dist/core/network.js +10 -0
  47. package/dist/core/open-browser.js +26 -0
  48. package/dist/core/output.js +68 -0
  49. package/dist/core/paths.js +26 -0
  50. package/dist/core/project.js +85 -0
  51. package/dist/core/prompt.js +140 -0
  52. package/dist/core/redact.js +45 -0
  53. package/dist/core/secure-fs.js +50 -0
  54. package/dist/core/text-file.js +12 -0
  55. package/dist/core/text.js +27 -0
  56. package/dist/core/time.js +84 -0
  57. package/dist/core/update-notice.js +142 -0
  58. package/dist/core/version.js +16 -0
  59. package/dist/core/warnings.js +10 -0
  60. package/dist/help.js +144 -0
  61. package/dist/modules/dashboard/assets.js +411 -0
  62. package/dist/modules/dashboard/data.js +565 -0
  63. package/dist/modules/dashboard/guard.js +75 -0
  64. package/dist/modules/dashboard/lock.js +93 -0
  65. package/dist/modules/dashboard/server.js +295 -0
  66. package/dist/modules/dashboard/views/html.js +114 -0
  67. package/dist/modules/dashboard/views/layout.js +59 -0
  68. package/dist/modules/dashboard/views/logs.js +122 -0
  69. package/dist/modules/dashboard/views/overview.js +316 -0
  70. package/dist/modules/dashboard/views/runs.js +486 -0
  71. package/dist/modules/dashboard/views/workers.js +157 -0
  72. package/dist/modules/workers/builtin/host-identity.js +59 -0
  73. package/dist/modules/workers/builtin/results-worker.js +37 -0
  74. package/dist/modules/workers/local-server.js +244 -0
  75. package/dist/modules/workers/module.js +98 -0
  76. package/dist/modules/workers/reload-result.js +16 -0
  77. package/dist/modules/workers/repeat-scheduler.js +165 -0
  78. package/dist/modules/workers/results/missing-paths.js +66 -0
  79. package/dist/modules/workers/results/query.js +175 -0
  80. package/dist/modules/workers/results/reader-input.js +134 -0
  81. package/dist/modules/workers/results/reader.js +430 -0
  82. package/dist/modules/workers/results/results-child.js +64 -0
  83. package/dist/modules/workers/results/shape.js +212 -0
  84. package/dist/modules/workers/results/store.js +336 -0
  85. package/dist/modules/workers/results/substitute.js +30 -0
  86. package/dist/modules/workers/runtime/env.js +43 -0
  87. package/dist/modules/workers/runtime/error-chain.js +109 -0
  88. package/dist/modules/workers/runtime/executor.js +289 -0
  89. package/dist/modules/workers/runtime/inspect.js +53 -0
  90. package/dist/modules/workers/runtime/protocol.js +1 -0
  91. package/dist/modules/workers/runtime/resolve.js +30 -0
  92. package/dist/modules/workers/runtime/schema.js +85 -0
  93. package/dist/modules/workers/runtime/thread.js +117 -0
  94. package/dist/modules/workers/start.js +265 -0
  95. package/dist/modules/workers/sync.js +241 -0
  96. package/dist/modules/workers/task-runner.js +892 -0
  97. package/dist/modules/workers/template.js +100 -0
  98. package/dist/modules/workers/worker-set.js +122 -0
  99. package/dist/modules/workers/workers.js +66 -0
  100. package/package.json +48 -5
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Maverick Works
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,83 @@
1
- # Temporary Holding Version
1
+ # 3minapi
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Run your own machine as a [3Min API](https://3minapi.com) host. An AI you work with (ChatGPT, Claude and other
4
+ MCP clients) creates tasks; this CLI fetches them and runs them with worker files you write — plain JavaScript or
5
+ TypeScript functions on your PC.
6
+
7
+ - Node.js **24.12 or newer**, no runtime dependencies, no install scripts
8
+ - Works on macOS, Linux and Windows
9
+
10
+ ## Install
11
+
12
+ ```sh
13
+ npm install -g @3minapi/cli
14
+ 3minapi --version
15
+ 3minapi doctor # checks Node.js, config, network and proxy settings
16
+ ```
17
+
18
+ `npx @3minapi/cli <command>` runs it without installing. On Windows PowerShell, if scripts are disabled, type
19
+ `3minapi.cmd` in place of `3minapi`.
20
+
21
+ ## Quick start
22
+
23
+ 1. **Create a host token** on the dashboard: [3minapi.com/dashboard/tasks/local](https://3minapi.com/dashboard/tasks/local).
24
+ It is shown once.
25
+ 2. **Log in** — the prompt is masked; the token is saved only on this machine.
26
+ ```sh
27
+ 3minapi login
28
+ ```
29
+ 3. **Create the project** (default `~/3minapi`) and a worker:
30
+ ```sh
31
+ 3minapi init
32
+ 3minapi worker new hello
33
+ 3minapi worker run hello --input '{"message":"hi"}' # runs locally, no API calls
34
+ ```
35
+ 4. **Start the host** — registers the workers and runs tasks until stopped:
36
+ ```sh
37
+ 3minapi start # or: 3minapi start --detach, then 3minapi stop
38
+ ```
39
+
40
+ Ask your AI to create a task for the `hello` worker; `3minapi status` and `3minapi logs` show what happened.
41
+
42
+ ## A worker
43
+
44
+ One file in `workers/` is one worker. The AI reads `description`; `handler` gets the input the AI sent.
45
+
46
+ ```js
47
+ export const description = 'Says hello. input: { message: string }';
48
+
49
+ export async function handler(input, ctx) {
50
+ ctx.log(`got ${input.message}`);
51
+ return { reply: `hello, ${input.message}` };
52
+ }
53
+ ```
54
+
55
+ Returning completes the task with that value; throwing fails it with the error message. `ctx.signal` aborts on
56
+ cancel or timeout. `3minapi worker new` writes a template that documents every option.
57
+
58
+ ## Using it from an AI coding agent
59
+
60
+ `3minapi init` writes `AGENTS.md` (and a `CLAUDE.md` that imports it) into the project, so Claude Code, Codex and
61
+ other agents know which files to edit, which commands to run and which files never to touch. The same text is
62
+ printed by:
63
+
64
+ ```sh
65
+ 3minapi guide
66
+ ```
67
+
68
+ Every command takes `--json` for machine-readable output. Log in yourself — never paste the host token into a chat.
69
+
70
+ ## Update
71
+
72
+ ```sh
73
+ 3minapi stop # if start is running
74
+ npm install -g @3minapi/cli@latest
75
+ ```
76
+
77
+ The CLI tells you when a newer version is recommended, and stops with exit code 6 when it is too old to connect.
78
+
79
+ ## Links
80
+
81
+ - Guide: [3minapi.com](https://3minapi.com)
82
+ - Security policy: [SECURITY.md](SECURITY.md)
83
+ - License: [MIT](LICENSE)
package/dist/bin.js ADDED
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+ import { EXIT_GRACE_MS, MIN_NODE_VERSION } from "./core/domain/constants.js";
3
+ import { raiseConnectAttemptTimeout } from "./core/network.js";
4
+ import { satisfiesMinNode } from "./core/version.js";
5
+ import { suppressSqliteExperimentalWarning } from "./core/warnings.js";
6
+ if (!satisfiesMinNode(process.versions.node)) {
7
+ const message = `3minapi requires Node.js ${MIN_NODE_VERSION} or later (you have ${process.versions.node}).\n` +
8
+ 'Install the current Node.js 24 LTS from https://nodejs.org and try again.';
9
+ process.stderr.write(`error: ${message}\n`);
10
+ if (process.argv.includes('--json'))
11
+ process.stdout.write(JSON.stringify({ error: { kind: 'config', message, exitCode: 3 } }, null, 2) + '\n');
12
+ process.exit(3);
13
+ }
14
+ suppressSqliteExperimentalWarning();
15
+ raiseConnectAttemptTimeout({ execArgv: process.execArgv, nodeOptions: process.env.NODE_OPTIONS });
16
+ const interrupt = new AbortController();
17
+ let claimed = false;
18
+ const onSignal = () => {
19
+ if (claimed && !interrupt.signal.aborted)
20
+ interrupt.abort();
21
+ else
22
+ process.exit(130);
23
+ };
24
+ process.on('SIGINT', onSignal);
25
+ process.on('SIGTERM', onSignal);
26
+ for (const stream of [process.stdout, process.stderr]) {
27
+ stream.on('error', (err) => {
28
+ if (err.code === 'EPIPE')
29
+ process.exit(0);
30
+ throw err;
31
+ });
32
+ }
33
+ const { main } = await import("./cli.js");
34
+ process.exitCode = await main(process.argv.slice(2), {
35
+ stdout: process.stdout,
36
+ stderr: process.stderr,
37
+ stdin: process.stdin,
38
+ env: process.env,
39
+ cwd: process.cwd(),
40
+ platform: process.platform,
41
+ home: (await import('node:os')).homedir(),
42
+ signal: interrupt.signal,
43
+ claimInterrupt: () => {
44
+ claimed = true;
45
+ }
46
+ });
47
+ const exitWhenFlushed = () => {
48
+ if (process.stdout.writableLength > 0 || process.stderr.writableLength > 0) {
49
+ setTimeout(exitWhenFlushed, EXIT_GRACE_MS).unref();
50
+ return;
51
+ }
52
+ process.exit();
53
+ };
54
+ setTimeout(exitWhenFlushed, EXIT_GRACE_MS).unref();
package/dist/cli.js ADDED
@@ -0,0 +1,241 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { parseArgs } from 'node:util';
3
+ import { commands } from "./commands/index.js";
4
+ import { loadConfig } from "./core/config.js";
5
+ import { describeError, errorDocument } from "./core/error-report.js";
6
+ import { BIN, CliError, ExitCode, UsageError } from "./core/domain/index.js";
7
+ import { Output } from "./core/output.js";
8
+ import { resolveDirs } from "./core/paths.js";
9
+ import { resolveProject, writeProjectRecord } from "./core/project.js";
10
+ import { GLOBAL_OPTIONS, commandHelp, commandHelpJson, groupHelp, groupHelpJson, rootHelp, rootHelpJson, suggest } from "./help.js";
11
+ export const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
12
+ export async function main(argv, io) {
13
+ const out = new Output({
14
+ stdout: io.stdout,
15
+ stderr: io.stderr,
16
+ env: io.env,
17
+ json: argv.includes('--json'),
18
+ color: argv.includes('--no-color') ? false : argv.includes('--color') ? true : undefined
19
+ });
20
+ try {
21
+ return await dispatch(argv, io, out);
22
+ }
23
+ catch (err) {
24
+ const report = describeError(err);
25
+ if (report.kind === 'bug') {
26
+ out.error(`unexpected error: ${report.stack}`);
27
+ out.info('This is a bug in the CLI. Please report it with the output above.');
28
+ }
29
+ else {
30
+ out.error(report.message);
31
+ }
32
+ out.errorResult(errorDocument(report));
33
+ return report.exitCode;
34
+ }
35
+ }
36
+ async function dispatch(argv, io, out) {
37
+ const index = commandIndex(argv);
38
+ const name = index === -1 ? undefined : argv[index];
39
+ let rest = index === -1 ? argv : [...argv.slice(0, index), ...argv.slice(index + 1)];
40
+ const showRoot = () => out.result(rootHelpJson(commands, VERSION), rootHelp(commands, VERSION));
41
+ const showCommand = (c) => c.subcommands
42
+ ? out.result(groupHelpJson(c), groupHelp(c))
43
+ : out.result(commandHelpJson(c), commandHelp(c));
44
+ if (name === undefined) {
45
+ const { values } = parse(rest, GLOBAL_OPTIONS, false, null);
46
+ if (values.version) {
47
+ out.result({ version: VERSION }, VERSION);
48
+ return ExitCode.Ok;
49
+ }
50
+ showRoot();
51
+ return ExitCode.Ok;
52
+ }
53
+ if (name === 'help') {
54
+ const { positionals } = parse(rest, GLOBAL_OPTIONS, true, null);
55
+ if (positionals.length === 0) {
56
+ showRoot();
57
+ return ExitCode.Ok;
58
+ }
59
+ let target = findCommand(positionals[0]);
60
+ for (const p of positionals.slice(1)) {
61
+ if (!target.subcommands)
62
+ break;
63
+ target = findSub(target, p);
64
+ }
65
+ showCommand(target);
66
+ return ExitCode.Ok;
67
+ }
68
+ let cmd = findCommand(name);
69
+ while (cmd.subcommands) {
70
+ const i = commandIndex(rest);
71
+ if (i === -1) {
72
+ showCommand(cmd);
73
+ return ExitCode.Ok;
74
+ }
75
+ const sub = findSub(cmd, rest[i]);
76
+ rest = [...rest.slice(0, i), ...rest.slice(i + 1)];
77
+ cmd = sub;
78
+ }
79
+ if (rest.includes('--help') || rest.includes('-h')) {
80
+ showCommand(cmd);
81
+ return ExitCode.Ok;
82
+ }
83
+ const { values, positionals } = parse(rest, { ...GLOBAL_OPTIONS, ...cmd.options }, true, cmd.name);
84
+ if (values.version) {
85
+ out.result({ version: VERSION }, VERSION);
86
+ return ExitCode.Ok;
87
+ }
88
+ if (positionals.length < cmd.args.min || positionals.length > cmd.args.max) {
89
+ throw new UsageError(`usage: ${BIN} ${cmd.name} ${cmd.usage}\nRun '${BIN} help ${cmd.name}' for details.`);
90
+ }
91
+ const dirs = resolveDirs({ platform: io.platform, env: io.env, home: io.home });
92
+ let resolved;
93
+ let configWarned = false;
94
+ const ctx = {
95
+ positionals,
96
+ values,
97
+ out,
98
+ stdin: io.stdin,
99
+ env: io.env,
100
+ cwd: io.cwd,
101
+ home: io.home,
102
+ platform: io.platform,
103
+ dirs,
104
+ version: VERSION,
105
+ now: io.now ?? Date.now,
106
+ signal: io.signal ?? new AbortController().signal,
107
+ claimInterrupt: () => io.claimInterrupt?.(),
108
+ chdir: io.chdir ?? ((dir) => process.chdir(dir)),
109
+ loadConfig: () => {
110
+ const config = loadConfig({
111
+ cwd: io.cwd,
112
+ env: io.env,
113
+ configDir: dirs.config,
114
+ configFile: values.config,
115
+ ...(values.config === undefined ? { projectFile: hostProject() } : {}),
116
+ flags: configFlags(values, cmd.name)
117
+ });
118
+ if (!configWarned && cmd.name !== 'doctor') {
119
+ configWarned = true;
120
+ for (const w of config.warnings)
121
+ out.warn(w);
122
+ }
123
+ return config;
124
+ }
125
+ };
126
+ return cmd.run(ctx);
127
+ function hostProject() {
128
+ if (resolved !== undefined)
129
+ return resolved;
130
+ const r = resolveProject({
131
+ cwd: io.cwd,
132
+ dataDir: dirs.data,
133
+ platform: io.platform,
134
+ home: io.home
135
+ });
136
+ if (r.record !== null) {
137
+ try {
138
+ writeProjectRecord(dirs.data, r.record, (io.now ?? Date.now)());
139
+ }
140
+ catch (err) {
141
+ out.warn(`could not record this host's project: ${err.message}`);
142
+ }
143
+ }
144
+ for (const notice of r.notices)
145
+ out.warn(notice);
146
+ resolved = r.projectFile;
147
+ return resolved;
148
+ }
149
+ }
150
+ export function configFlags(values, command) {
151
+ const flags = {};
152
+ if (typeof values['api-url'] === 'string')
153
+ flags.apiUrl = values['api-url'];
154
+ if (command === 'start' && typeof values.port === 'string')
155
+ flags.localServerPort = values.port;
156
+ if (command === 'start' && typeof values.mode === 'string')
157
+ flags.receiveMode = values.mode;
158
+ return flags;
159
+ }
160
+ function commandIndex(argv) {
161
+ for (let i = 0; i < argv.length; i++) {
162
+ const arg = argv[i];
163
+ if (arg === '--')
164
+ return i + 1 < argv.length ? i + 1 : -1;
165
+ if (!arg.startsWith('-') || arg === '-')
166
+ return i;
167
+ const long = arg.startsWith('--') ? arg.slice(2) : undefined;
168
+ if (long !== undefined && !long.includes('=') && GLOBAL_OPTIONS[long]?.type === 'string')
169
+ i++;
170
+ }
171
+ return -1;
172
+ }
173
+ function findCommand(name) {
174
+ const cmd = commands.find((c) => c.name === name);
175
+ if (cmd)
176
+ return cmd;
177
+ const guess = suggest(name, commands.map((c) => c.name));
178
+ throw new UsageError(`unknown command "${name}".` +
179
+ (guess ? ` Did you mean "${guess}"?` : '') +
180
+ `\nRun '${BIN} --help' to see all commands.`);
181
+ }
182
+ function findSub(group, name) {
183
+ const subs = group.subcommands ?? [];
184
+ const short = (c) => c.name.slice(group.name.length + 1);
185
+ const sub = subs.find((c) => short(c) === name);
186
+ if (sub)
187
+ return sub;
188
+ const guess = suggest(name, subs.map(short));
189
+ throw new UsageError(`unknown command "${group.name} ${name}".` +
190
+ (guess ? ` Did you mean "${group.name} ${guess}"?` : '') +
191
+ `\nRun '${BIN} ${group.name} --help' to see its commands.`);
192
+ }
193
+ function parse(args, options, allowPositionals, command) {
194
+ const config = Object.fromEntries(Object.entries(options).map(([k, { description: _d, valueName: _v, negated: _n, ...spec }]) => [
195
+ k,
196
+ spec
197
+ ]));
198
+ let parsed;
199
+ try {
200
+ parsed = parseArgs({
201
+ args,
202
+ options: config,
203
+ strict: true,
204
+ allowPositionals,
205
+ allowNegative: true,
206
+ tokens: true
207
+ });
208
+ }
209
+ catch (err) {
210
+ if (err instanceof TypeError &&
211
+ 'code' in err &&
212
+ String(err.code).startsWith('ERR_PARSE_ARGS')) {
213
+ throw new UsageError(parseErrorMessage(err, String(err.code), options, command));
214
+ }
215
+ throw err;
216
+ }
217
+ for (const token of parsed.tokens) {
218
+ if (token.kind !== 'option' || !token.rawName.startsWith('--no-'))
219
+ continue;
220
+ const spec = options[token.name];
221
+ if (spec?.negated || token.name === 'color')
222
+ continue;
223
+ throw new UsageError(parseErrorMessage(new Error(`Unknown option '${token.rawName}'`), 'ERR_PARSE_ARGS_UNKNOWN_OPTION', options, command));
224
+ }
225
+ return { values: parsed.values, positionals: parsed.positionals };
226
+ }
227
+ function parseErrorMessage(err, code, options, command) {
228
+ const help = command ? `Run '${BIN} help ${command}' for its options.` : `Run '${BIN} --help'.`;
229
+ const quoted = /'([^']+)'/.exec(err.message)?.[1];
230
+ if (code === 'ERR_PARSE_ARGS_UNKNOWN_OPTION' && quoted) {
231
+ const names = Object.entries(options).flatMap(([name, spec]) => [
232
+ `--${name}`,
233
+ ...(spec.short ? [`-${spec.short}`] : []),
234
+ ...(spec.negated || name === 'color' ? [`--no-${name}`] : [])
235
+ ]);
236
+ const guess = suggest(quoted, names);
237
+ return `unknown option "${quoted}".${guess ? ` Did you mean "${guess}"?` : ''}\n${help}`;
238
+ }
239
+ const first = err.message.split(/\.\s/)[0].replace(/\.$/, '');
240
+ return `${first}.\n${help}`;
241
+ }