@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.
- package/LICENSE +21 -0
- package/README.md +82 -2
- package/dist/bin.js +54 -0
- package/dist/cli.js +241 -0
- package/dist/commands/auth.js +573 -0
- package/dist/commands/common.js +48 -0
- package/dist/commands/dashboard.js +298 -0
- package/dist/commands/delete.js +167 -0
- package/dist/commands/doctor.js +326 -0
- package/dist/commands/guide.js +84 -0
- package/dist/commands/index.js +28 -0
- package/dist/commands/init.js +248 -0
- package/dist/commands/logs.js +154 -0
- package/dist/commands/new.js +109 -0
- package/dist/commands/paths.js +64 -0
- package/dist/commands/reload.js +32 -0
- package/dist/commands/results.js +445 -0
- package/dist/commands/start.js +443 -0
- package/dist/commands/stop.js +68 -0
- package/dist/commands/tasks.js +93 -0
- package/dist/commands/worker.js +178 -0
- package/dist/core/api.js +132 -0
- package/dist/core/config.js +188 -0
- package/dist/core/control-db.js +462 -0
- package/dist/core/credentials.js +75 -0
- package/dist/core/domain/constants.js +121 -0
- package/dist/core/domain/contract.js +102 -0
- package/dist/core/domain/errors.js +57 -0
- package/dist/core/domain/index.js +3 -0
- package/dist/core/domain/types.js +1 -0
- package/dist/core/error-report.js +103 -0
- package/dist/core/host/detach.js +49 -0
- package/dist/core/host/live-connection.js +270 -0
- package/dist/core/host/live-receiver.js +264 -0
- package/dist/core/host/module.js +1 -0
- package/dist/core/host/poller.js +113 -0
- package/dist/core/host/pull-step.js +54 -0
- package/dist/core/host/receiver.js +1 -0
- package/dist/core/host/reload.js +34 -0
- package/dist/core/host/runner-lock.js +245 -0
- package/dist/core/host/session.js +84 -0
- package/dist/core/host/usage.js +20 -0
- package/dist/core/log-read.js +59 -0
- package/dist/core/logger.js +206 -0
- package/dist/core/names.js +27 -0
- package/dist/core/network.js +10 -0
- package/dist/core/open-browser.js +26 -0
- package/dist/core/output.js +68 -0
- package/dist/core/paths.js +26 -0
- package/dist/core/project.js +85 -0
- package/dist/core/prompt.js +140 -0
- package/dist/core/redact.js +45 -0
- package/dist/core/secure-fs.js +50 -0
- package/dist/core/text-file.js +12 -0
- package/dist/core/text.js +27 -0
- package/dist/core/time.js +84 -0
- package/dist/core/update-notice.js +142 -0
- package/dist/core/version.js +16 -0
- package/dist/core/warnings.js +10 -0
- package/dist/help.js +144 -0
- package/dist/modules/dashboard/assets.js +411 -0
- package/dist/modules/dashboard/data.js +565 -0
- package/dist/modules/dashboard/guard.js +75 -0
- package/dist/modules/dashboard/lock.js +93 -0
- package/dist/modules/dashboard/server.js +295 -0
- package/dist/modules/dashboard/views/html.js +114 -0
- package/dist/modules/dashboard/views/layout.js +59 -0
- package/dist/modules/dashboard/views/logs.js +122 -0
- package/dist/modules/dashboard/views/overview.js +316 -0
- package/dist/modules/dashboard/views/runs.js +486 -0
- package/dist/modules/dashboard/views/workers.js +157 -0
- package/dist/modules/workers/builtin/host-identity.js +59 -0
- package/dist/modules/workers/builtin/results-worker.js +37 -0
- package/dist/modules/workers/local-server.js +244 -0
- package/dist/modules/workers/module.js +98 -0
- package/dist/modules/workers/reload-result.js +16 -0
- package/dist/modules/workers/repeat-scheduler.js +165 -0
- package/dist/modules/workers/results/missing-paths.js +66 -0
- package/dist/modules/workers/results/query.js +175 -0
- package/dist/modules/workers/results/reader-input.js +134 -0
- package/dist/modules/workers/results/reader.js +430 -0
- package/dist/modules/workers/results/results-child.js +64 -0
- package/dist/modules/workers/results/shape.js +212 -0
- package/dist/modules/workers/results/store.js +336 -0
- package/dist/modules/workers/results/substitute.js +30 -0
- package/dist/modules/workers/runtime/env.js +43 -0
- package/dist/modules/workers/runtime/error-chain.js +109 -0
- package/dist/modules/workers/runtime/executor.js +289 -0
- package/dist/modules/workers/runtime/inspect.js +53 -0
- package/dist/modules/workers/runtime/protocol.js +1 -0
- package/dist/modules/workers/runtime/resolve.js +30 -0
- package/dist/modules/workers/runtime/schema.js +85 -0
- package/dist/modules/workers/runtime/thread.js +117 -0
- package/dist/modules/workers/start.js +265 -0
- package/dist/modules/workers/sync.js +241 -0
- package/dist/modules/workers/task-runner.js +892 -0
- package/dist/modules/workers/template.js +100 -0
- package/dist/modules/workers/worker-set.js +122 -0
- package/dist/modules/workers/workers.js +66 -0
- package/package.json +48 -5
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const HEADER = (name) => `// Worker "${name}" — runs on this machine as a local worker when an AI creates a task for it.
|
|
2
|
+
// One file = one worker. The file name is the worker name.
|
|
3
|
+
// Code edits take effect on the next task. A renamed file or a changed description
|
|
4
|
+
// reaches 3Min API on '3minapi reload' (or the next '3minapi start').`;
|
|
5
|
+
export const SETTINGS = `/**
|
|
6
|
+
* What this worker does (max 500 characters). This is the only thing the AI sees
|
|
7
|
+
* about the worker: it decides when to call it and what input to send from this text.
|
|
8
|
+
* To tell the AI the input shape, write it here (inputSchema below is not sent).
|
|
9
|
+
* EXAMPLE — describe your own worker.
|
|
10
|
+
*/
|
|
11
|
+
export const description = 'Echo back the message it receives. input: { message: string }';
|
|
12
|
+
|
|
13
|
+
/** Optional. The run is stopped and reported as failed after this long (default 60s). */
|
|
14
|
+
export const timeoutMs = 60_000;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Optional. JSON Schema for the task input — an EXAMPLE matching the handler below.
|
|
18
|
+
* Replace it with your own fields, or delete it to accept any input.
|
|
19
|
+
* Checked on this machine only: it is not sent to 3Min API, so the AI does not see it.
|
|
20
|
+
* A task whose input does not match is reported as failed before the handler runs.
|
|
21
|
+
* Supported keywords: type, properties, required, enum, items.
|
|
22
|
+
*/
|
|
23
|
+
export const inputSchema = {
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
message: { type: 'string', description: 'Text to echo' } // example field
|
|
27
|
+
},
|
|
28
|
+
required: ['message']
|
|
29
|
+
};`;
|
|
30
|
+
export const HANDLER_DOC = `/**
|
|
31
|
+
* Runs once per task.
|
|
32
|
+
*
|
|
33
|
+
* input — the JSON the AI sent with the task (any shape, up to 100 KB).
|
|
34
|
+
*
|
|
35
|
+
* How the task ends:
|
|
36
|
+
* return value → completed; the value becomes the result (JSON). Every result
|
|
37
|
+
* is saved in the project's data.db ('3minapi worker results').
|
|
38
|
+
* Over 100 KB: a local worker sends the AI a summary instead;
|
|
39
|
+
* a public worker's task fails. A top-level "_3minapi" key is
|
|
40
|
+
* reserved — the task fails.
|
|
41
|
+
* throw new Error() → failed; only the message is sent to the AI, with its causes
|
|
42
|
+
* (err.cause) on the same line, cut at 4,000 characters. The
|
|
43
|
+
* stack trace stays in the local log.
|
|
44
|
+
* timeoutMs passed → failed
|
|
45
|
+
* cancelled → set by the requester, not by the worker. ctx.signal fires:
|
|
46
|
+
* stop work and return. The return value is ignored.
|
|
47
|
+
*
|
|
48
|
+
* ctx:
|
|
49
|
+
* ctx.log(msg) write to this worker's log ('3minapi worker logs <name>')
|
|
50
|
+
* await ctx.report(m) send a progress message to the AI while working
|
|
51
|
+
* (max 256 characters; each report counts as one API call)
|
|
52
|
+
* ctx.signal AbortSignal — aborted on cancel or timeout
|
|
53
|
+
* ctx.env environment variables, including the project's .env
|
|
54
|
+
* (THREEMINAPI_* variables are removed)
|
|
55
|
+
*
|
|
56
|
+
* Need to keep data between tasks? Use the project's data.db (made by '3minapi init').
|
|
57
|
+
* 3minapi saves results there in tables starting with _3minapi_ — read them if you
|
|
58
|
+
* like, but do not write them. Open it with Node's built-in node:sqlite:
|
|
59
|
+
* import { DatabaseSync } from 'node:sqlite';
|
|
60
|
+
* const db = new DatabaseSync(new URL('../data.db', import.meta.url), { timeout: 5000 });
|
|
61
|
+
* db.exec('PRAGMA journal_mode = WAL'); // tasks can run at the same time
|
|
62
|
+
* try { ... } finally { db.close(); }
|
|
63
|
+
* The URL is relative to this file (default workers/ folder), so it does not depend on
|
|
64
|
+
* the current folder.
|
|
65
|
+
*/`;
|
|
66
|
+
const HANDLER_BODY = ` ctx.log(\`received: \${input.message}\`);
|
|
67
|
+
|
|
68
|
+
// await ctx.report('halfway there');
|
|
69
|
+
// if (!input.message.trim()) throw new Error('message is empty');
|
|
70
|
+
|
|
71
|
+
return { echo: input.message };
|
|
72
|
+
}
|
|
73
|
+
`;
|
|
74
|
+
export function jsTemplate(name) {
|
|
75
|
+
return `${HEADER(name)}
|
|
76
|
+
|
|
77
|
+
${SETTINGS}
|
|
78
|
+
|
|
79
|
+
${HANDLER_DOC}
|
|
80
|
+
export async function handler(input, ctx) {
|
|
81
|
+
${HANDLER_BODY}`;
|
|
82
|
+
}
|
|
83
|
+
export function tsTemplate(name) {
|
|
84
|
+
return `${HEADER(name)}
|
|
85
|
+
// TypeScript runs without a build step (Node type stripping): use plain type
|
|
86
|
+
// annotations, not enums or namespaces. Types are not checked at run time.
|
|
87
|
+
|
|
88
|
+
interface Context {
|
|
89
|
+
log(message: string): void;
|
|
90
|
+
report(message: string): Promise<void>;
|
|
91
|
+
signal: AbortSignal;
|
|
92
|
+
env: Record<string, string | undefined>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
${SETTINGS}
|
|
96
|
+
|
|
97
|
+
${HANDLER_DOC}
|
|
98
|
+
export async function handler(input: any, ctx: Context) {
|
|
99
|
+
${HANDLER_BODY}`;
|
|
100
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
export class WorkerSet {
|
|
2
|
+
local;
|
|
3
|
+
rejected = new Map();
|
|
4
|
+
pushServed = new Set();
|
|
5
|
+
deleted = new Set();
|
|
6
|
+
unsent = new Set();
|
|
7
|
+
serverSeen = false;
|
|
8
|
+
server = new Set();
|
|
9
|
+
builtinName;
|
|
10
|
+
constructor(localNames, builtin = null) {
|
|
11
|
+
this.local = new Set(localNames);
|
|
12
|
+
this.builtinName = builtin;
|
|
13
|
+
}
|
|
14
|
+
get builtin() {
|
|
15
|
+
return this.builtinName;
|
|
16
|
+
}
|
|
17
|
+
get builtinActive() {
|
|
18
|
+
return this.builtinName !== null && this.isActive(this.builtinName);
|
|
19
|
+
}
|
|
20
|
+
applySync(res, ctx, unsent = []) {
|
|
21
|
+
for (const name of unsent)
|
|
22
|
+
this.unsent.add(name);
|
|
23
|
+
const real = [];
|
|
24
|
+
for (const r of res.rejected) {
|
|
25
|
+
if (ctx.public &&
|
|
26
|
+
r.code === 'worker_name_taken' &&
|
|
27
|
+
r.conflict?.kind === 'push' &&
|
|
28
|
+
ctx.hasPushKeys(r.name)) {
|
|
29
|
+
this.pushServed.add(r.name);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
this.rejected.set(r.name, r);
|
|
33
|
+
real.push(r);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
this.server = new Set(res.workers.map((w) => w.name));
|
|
37
|
+
return real;
|
|
38
|
+
}
|
|
39
|
+
resync(localNames, res, ctx, unsent = [], builtin = this.builtinName) {
|
|
40
|
+
this.local = new Set(localNames);
|
|
41
|
+
this.builtinName = builtin;
|
|
42
|
+
this.rejected.clear();
|
|
43
|
+
this.pushServed.clear();
|
|
44
|
+
this.unsent.clear();
|
|
45
|
+
for (const w of res.workers)
|
|
46
|
+
this.deleted.delete(w.name);
|
|
47
|
+
this.serverSeen = false;
|
|
48
|
+
return this.applySync(res, ctx, unsent);
|
|
49
|
+
}
|
|
50
|
+
applyServerList(names) {
|
|
51
|
+
const before = this.activeNames({ builtin: true });
|
|
52
|
+
const first = !this.serverSeen;
|
|
53
|
+
this.server = new Set(names);
|
|
54
|
+
this.serverSeen = true;
|
|
55
|
+
const deleted = [];
|
|
56
|
+
const unregistered = [];
|
|
57
|
+
for (const name of before) {
|
|
58
|
+
if (this.isActive(name))
|
|
59
|
+
continue;
|
|
60
|
+
if (first && this.unsent.has(name)) {
|
|
61
|
+
unregistered.push(name);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
this.deleted.add(name);
|
|
65
|
+
deleted.push(name);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { deleted, unregistered };
|
|
69
|
+
}
|
|
70
|
+
dropLocal(names) {
|
|
71
|
+
const dropped = [];
|
|
72
|
+
for (const name of names) {
|
|
73
|
+
if (!this.isActive(name))
|
|
74
|
+
continue;
|
|
75
|
+
this.deleted.add(name);
|
|
76
|
+
this.server.delete(name);
|
|
77
|
+
dropped.push(name);
|
|
78
|
+
}
|
|
79
|
+
return dropped.sort();
|
|
80
|
+
}
|
|
81
|
+
pendingNames() {
|
|
82
|
+
return this.serverSeen ? [] : [...this.unsent].filter((n) => this.local.has(n)).sort();
|
|
83
|
+
}
|
|
84
|
+
isActive(name) {
|
|
85
|
+
if (!this.local.has(name) && name !== this.builtinName)
|
|
86
|
+
return false;
|
|
87
|
+
if (this.pushServed.has(name))
|
|
88
|
+
return true;
|
|
89
|
+
if (this.rejected.has(name) || this.deleted.has(name))
|
|
90
|
+
return false;
|
|
91
|
+
if (!this.serverSeen && this.unsent.has(name))
|
|
92
|
+
return true;
|
|
93
|
+
return this.server.has(name);
|
|
94
|
+
}
|
|
95
|
+
inactiveReason(name) {
|
|
96
|
+
if (this.isActive(name))
|
|
97
|
+
return null;
|
|
98
|
+
if (this.rejected.has(name))
|
|
99
|
+
return 'rejected';
|
|
100
|
+
if (this.deleted.has(name))
|
|
101
|
+
return 'deleted_on_server';
|
|
102
|
+
return 'not_on_server';
|
|
103
|
+
}
|
|
104
|
+
activeNames({ builtin = false } = {}) {
|
|
105
|
+
const names = [...this.local];
|
|
106
|
+
if (builtin && this.builtinName !== null)
|
|
107
|
+
names.push(this.builtinName);
|
|
108
|
+
return names.filter((n) => this.isActive(n)).sort();
|
|
109
|
+
}
|
|
110
|
+
get activeCount() {
|
|
111
|
+
return this.activeNames().length;
|
|
112
|
+
}
|
|
113
|
+
get pullActiveCount() {
|
|
114
|
+
return this.activeNames().filter((n) => !this.pushServed.has(n)).length;
|
|
115
|
+
}
|
|
116
|
+
pushServedNames() {
|
|
117
|
+
return [...this.pushServed].sort();
|
|
118
|
+
}
|
|
119
|
+
deletedNames() {
|
|
120
|
+
return [...this.deleted].sort();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { readdirSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { CliError, WORKER_DESCRIPTION_MAX_CHARS, WORKER_EXTENSIONS } from "../../core/domain/index.js";
|
|
4
|
+
import { checkWorkerName } from "../../core/names.js";
|
|
5
|
+
import { inspectWorker } from "./runtime/inspect.js";
|
|
6
|
+
const INSPECT_CONCURRENCY = 4;
|
|
7
|
+
export function workerFiles(dir) {
|
|
8
|
+
let entries;
|
|
9
|
+
try {
|
|
10
|
+
entries = readdirSync(dir);
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
throw new CliError(`cannot read workers folder ${dir}: ${err.message}`);
|
|
14
|
+
}
|
|
15
|
+
const byName = new Map();
|
|
16
|
+
for (const entry of entries.sort()) {
|
|
17
|
+
const ext = path.extname(entry);
|
|
18
|
+
if (!WORKER_EXTENSIONS.includes(ext))
|
|
19
|
+
continue;
|
|
20
|
+
const name = entry.slice(0, -ext.length);
|
|
21
|
+
byName.set(name, [...(byName.get(name) ?? []), path.join(dir, entry)]);
|
|
22
|
+
}
|
|
23
|
+
return byName;
|
|
24
|
+
}
|
|
25
|
+
export async function collectWorkers(dir, env) {
|
|
26
|
+
const workers = [...workerFiles(dir)].map(([name, files]) => ({
|
|
27
|
+
name,
|
|
28
|
+
files,
|
|
29
|
+
problem: null,
|
|
30
|
+
message: null,
|
|
31
|
+
meta: null
|
|
32
|
+
}));
|
|
33
|
+
const toInspect = [];
|
|
34
|
+
for (const w of workers) {
|
|
35
|
+
const check = checkWorkerName(w.name);
|
|
36
|
+
if (!check.ok) {
|
|
37
|
+
w.problem = 'invalid_name';
|
|
38
|
+
w.message = `not a valid worker name (${check.reason}) — tasks cannot reach it`;
|
|
39
|
+
}
|
|
40
|
+
else if (w.files.length > 1) {
|
|
41
|
+
w.problem = 'duplicate';
|
|
42
|
+
w.message = 'more than one file with this name — keep only one';
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
toInspect.push(w);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
let next = 0;
|
|
49
|
+
const lane = async () => {
|
|
50
|
+
while (next < toInspect.length) {
|
|
51
|
+
const w = toInspect[next++];
|
|
52
|
+
const r = await inspectWorker(w.files[0], env);
|
|
53
|
+
if (r.ok)
|
|
54
|
+
w.meta = r.meta;
|
|
55
|
+
else {
|
|
56
|
+
w.problem = 'load_failed';
|
|
57
|
+
w.message = `${r.reason}: ${r.message}`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
await Promise.all(Array.from({ length: Math.min(INSPECT_CONCURRENCY, toInspect.length) }, lane));
|
|
62
|
+
return workers;
|
|
63
|
+
}
|
|
64
|
+
export function descriptionTooLong(description) {
|
|
65
|
+
return Array.from(description).length > WORKER_DESCRIPTION_MAX_CHARS;
|
|
66
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@3minapi/cli",
|
|
3
|
+
"version": "0.22.0",
|
|
4
|
+
"description": "Run your machine as a 3Min API host — the CLI that fetches tasks from your AI and runs them with your own worker files",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"3minapi",
|
|
7
|
+
"mcp",
|
|
8
|
+
"ai",
|
|
9
|
+
"worker",
|
|
10
|
+
"cli",
|
|
11
|
+
"tasks"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "Maverick Works",
|
|
15
|
+
"homepage": "https://3minapi.com",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/maverick-works-dev/3minapi-cli.git"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/maverick-works-dev/3minapi-cli/issues"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"bin": {
|
|
25
|
+
"3minapi": "dist/bin.js"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=24.12.0"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"dev": "node src/bin.ts",
|
|
38
|
+
"build": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json",
|
|
39
|
+
"prepack": "npm run build",
|
|
40
|
+
"test": "node --test --test-timeout=60000",
|
|
41
|
+
"lint": "tsc --noEmit && prettier --check .",
|
|
42
|
+
"format": "prettier --write ."
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "24.10.1",
|
|
46
|
+
"prettier": "3.7.3",
|
|
47
|
+
"typescript": "5.9.3"
|
|
48
|
+
}
|
|
49
|
+
}
|