@noimosai/platform-cli 0.1.2 → 0.1.4
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/README.md +12 -56
- package/bin/noimos-platform.mjs +3 -4
- package/dist/cli/cli.js +477 -87
- package/dist/cli/command-help.js +349 -23
- package/dist/cli/download.js +25 -25
- package/dist/cli/input-error.js +48 -0
- package/dist/cli/local-agents.js +199 -0
- package/dist/cli/mcp-supervisor.js +136 -0
- package/dist/cli/noimos-cli.js +72 -0
- package/dist/cli/project-archive.js +27 -0
- package/dist/cli/project-files.js +25 -0
- package/dist/cli/project-sync.js +259 -0
- package/dist/cli/setup-lock.js +1 -0
- package/dist/cli/setup.js +347 -0
- package/dist/cli/skills.js +142 -0
- package/dist/cli/team-harness-targets.js +337 -0
- package/dist/cli/team-harness.js +1562 -0
- package/dist/cli/team-resources.js +222 -0
- package/dist/cli/team-skills.js +457 -0
- package/dist/cli/update-bootstrap.js +437 -0
- package/dist/cli/update-release.js +212 -0
- package/dist/cli/version.js +1 -0
- package/dist/cli/wallet.js +85 -0
- package/dist/client/agent-run.js +1425 -0
- package/dist/client/browser.js +905 -0
- package/dist/client/distribution.js +2 -0
- package/dist/client/evm-wallet.js +214 -0
- package/dist/client/execution-context.js +14 -0
- package/dist/client/failure-recovery.js +128 -0
- package/dist/client/index.js +4 -225
- package/dist/client/local-input.js +33 -12
- package/dist/client/local-profile-lock.js +52 -0
- package/dist/client/local-profile.js +181 -0
- package/dist/client/local-projects.js +131 -0
- package/dist/client/local-setup.js +93 -0
- package/dist/client/realtime-voice.js +184 -0
- package/dist/client/tool-name-hints.js +48 -0
- package/dist/client/wallet-access.js +214 -0
- package/dist/mcp/agent-server.js +345 -0
- package/dist/mcp/context.js +86 -0
- package/dist/mcp/host-schema.js +76 -0
- package/dist/mcp/icon.js +1 -0
- package/dist/mcp/index.js +3 -0
- package/dist/mcp/instructions.js +26 -0
- package/dist/mcp/local-projects.js +42 -0
- package/dist/mcp/local-upload.js +39 -0
- package/dist/mcp/noimos-runtime.js +18 -0
- package/dist/mcp/noimos-server.js +38 -0
- package/dist/mcp/operations.js +40 -0
- package/dist/mcp/parallel-result.js +17 -0
- package/dist/mcp/project-sync.js +105 -0
- package/dist/mcp/results-reply.js +15 -0
- package/dist/mcp/runtime.js +81 -0
- package/dist/mcp/server.js +410 -0
- package/dist/mcp/team-skill.js +63 -0
- package/dist/mcp/update-notice.js +75 -0
- package/dist/mcp/version.js +1 -0
- package/dist/schema/accountRequestMethods.js +1 -0
- package/dist/schema/boundedBody.js +30 -0
- package/dist/schema/cronValidator.js +150 -0
- package/dist/schema/platformApiKey.js +14 -0
- package/dist/schema/platformAppAuth.js +11 -0
- package/dist/schema/platformBillingConstants.js +4 -0
- package/dist/schema/platformCapabilityFamilies.js +293 -0
- package/dist/schema/platformConnectionCatalog.js +39 -0
- package/dist/schema/platformConnectionPolicy.js +33 -0
- package/dist/schema/platformCustomTools.js +148 -0
- package/dist/schema/platformEndUserApi.js +62 -0
- package/dist/schema/platformFunding.js +10 -0
- package/dist/schema/platformHttpConstraints.js +12 -0
- package/dist/schema/platformLocalSetup.js +25 -0
- package/dist/schema/platformMcpTools.js +7 -0
- package/dist/schema/platformMetaTools.js +63 -0
- package/dist/schema/platformMonitorSchedule.js +123 -0
- package/dist/schema/platformMonitors.js +159 -0
- package/dist/schema/platformOAuth.js +13 -0
- package/dist/schema/platformProjectPermissions.js +33 -0
- package/dist/schema/platformPublicExecution.js +202 -21
- package/dist/schema/platformRoutes.js +122 -0
- package/dist/schema/platformSkills.js +11 -0
- package/dist/schema/platformTeamSecretName.js +3 -0
- package/dist/schema/platformTeamSkillSlug.js +1 -0
- package/dist/schema/platformTeamSkills.js +302 -0
- package/dist/schema/platformToolAccess.js +62 -0
- package/dist/schema/platformTriggers.js +153 -0
- package/dist/schema/platformWorkflows.js +100 -0
- package/dist/schema/scheduleTimeZone.js +15 -0
- package/dist/schema/toolDiscovery.js +71 -0
- package/dist/schema/toolInputRepair.js +122 -0
- package/dist/schema/toolResultContext.js +51 -0
- package/dist/schema/untrustedContent.js +35 -0
- package/npm-shrinkwrap.json +73 -0
- package/package.json +6 -3
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { access, open, realpath, stat } from "node:fs/promises";
|
|
4
|
+
import { delimiter, isAbsolute, join } from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { createInterface } from "node:readline";
|
|
7
|
+
import { once } from "node:events";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
const execute = promisify(execFile);
|
|
10
|
+
export const managedCliChildEnv = "RELVO_MANAGED_CHILD";
|
|
11
|
+
export function cleanAgentEnvironment(env, home) {
|
|
12
|
+
const result = { ...env, HOME: home };
|
|
13
|
+
for (const key of ["RELVO_API_KEY", "RELVO_API_URL", "RELVO_ENV", "NOIMOS_PLATFORM_API_KEY", "NOIMOS_PLATFORM_API_URL", "NOIMOS_PLATFORM_ENV", "RELVO_MCP_ALLOWED_UPLOAD_ROOTS", "NOIMOS_MCP_ALLOWED_UPLOAD_ROOTS", "RELVO_WALLET_PRIVATE_KEY", "RELVO_WALLET_AUTO_TOP_UP_USD"])
|
|
14
|
+
delete result[key];
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
export async function executable(name, env, keepPath = false) {
|
|
18
|
+
for (const directory of (env.PATH ?? "").split(delimiter).filter(isAbsolute)) {
|
|
19
|
+
try {
|
|
20
|
+
const path = await realpath(join(directory, name));
|
|
21
|
+
const info = await stat(path);
|
|
22
|
+
if (!info.isFile() || info.mode & 0o022 || typeof process.getuid === "function" && ![0, process.getuid()].includes(info.uid))
|
|
23
|
+
continue;
|
|
24
|
+
await access(path, constants.X_OK);
|
|
25
|
+
return keepPath ? join(directory, name) : path;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
throw new Error(`Install ${name} and make its executable available on PATH before setup.`);
|
|
32
|
+
}
|
|
33
|
+
export async function preflightAgent(client, env, home, registryRuntime = true, purpose = "setup") {
|
|
34
|
+
if (Number(process.versions.node.split(".")[0]) < 22)
|
|
35
|
+
throw new Error("Relvo setup requires Node.js 22 or newer.");
|
|
36
|
+
if (client === "terminal")
|
|
37
|
+
return {};
|
|
38
|
+
if (client === "claude" && env.CLAUDE_CONFIG_DIR !== undefined)
|
|
39
|
+
throw new Error("Relvo setup cannot safely inspect a custom Claude configuration directory. Use the default Claude user configuration for automatic setup.");
|
|
40
|
+
const agent = await executable(client, env), launcher = registryRuntime ? await executable("npx", env, true) : undefined;
|
|
41
|
+
try {
|
|
42
|
+
await execute(agent, ["--version"], { cwd: home, env: cleanAgentEnvironment(env, home), timeout: 15000, maxBuffer: 65536 });
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
throw new Error(`The ${client} executable could not be verified.${purpose === "setup" ? " No login request was created." : " Run relvo doctor again."}`);
|
|
46
|
+
}
|
|
47
|
+
return { agent, launcher };
|
|
48
|
+
}
|
|
49
|
+
export const agentNames = { claude: "Claude Code", codex: "Codex" };
|
|
50
|
+
export async function preflightAgents(requested, env, home, registryRuntime, report) {
|
|
51
|
+
if (requested !== "auto") {
|
|
52
|
+
const { agent, launcher } = await preflightAgent(requested, env, home, registryRuntime);
|
|
53
|
+
return requested === "terminal" || !agent ? [] : [{ client: requested, agent, launcher }];
|
|
54
|
+
}
|
|
55
|
+
await preflightAgent("terminal", env, home);
|
|
56
|
+
const prepared = [];
|
|
57
|
+
for (const client of ["claude", "codex"]) {
|
|
58
|
+
try {
|
|
59
|
+
await executable(client, env);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
report(`${agentNames[client]} was not found on PATH; skipped.`);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (client === "claude" && env.CLAUDE_CONFIG_DIR !== undefined) {
|
|
66
|
+
report("Claude Code uses a custom configuration directory, which automatic setup cannot inspect safely; skipped.");
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const { agent, launcher } = await preflightAgent(client, env, home, registryRuntime);
|
|
70
|
+
if (agent)
|
|
71
|
+
prepared.push({ client, agent, launcher });
|
|
72
|
+
}
|
|
73
|
+
if (!prepared.length)
|
|
74
|
+
report("Neither Claude Code nor Codex was found; setting up the Relvo CLI only.");
|
|
75
|
+
return prepared;
|
|
76
|
+
}
|
|
77
|
+
const entrySchema = z.object({ type: z.literal("stdio"), command: z.string(), args: z.array(z.string()).default([]), env: z.record(z.string(), z.string()).optional().nullable(), cwd: z.string().optional().nullable() });
|
|
78
|
+
export function registrationMatches(input, expected) {
|
|
79
|
+
const value = entrySchema.safeParse(input);
|
|
80
|
+
return value.success && value.data.command === expected.command && JSON.stringify(value.data.args) === JSON.stringify(expected.args) &&
|
|
81
|
+
!Object.keys(value.data.env ?? {}).length && !value.data.cwd;
|
|
82
|
+
}
|
|
83
|
+
export async function agentRegistration(client, agent, env, home, name = "relvo") {
|
|
84
|
+
if (client === "codex") {
|
|
85
|
+
let stdout;
|
|
86
|
+
try {
|
|
87
|
+
({ stdout } = await execute(agent, ["mcp", "list", "--json"], { cwd: home, env: cleanAgentEnvironment(env, home), timeout: 15000, maxBuffer: 1048576 }));
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
throw new Error("Codex MCP configuration could not be inspected. No server was overwritten.");
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const rows = z.array(z.object({ name: z.string(), transport: z.unknown() }).passthrough()).parse(JSON.parse(stdout));
|
|
94
|
+
return rows.find((row) => row.name === name)?.transport ?? null;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
throw new Error("Codex returned an unexpected MCP configuration. No server was overwritten.");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
let handle;
|
|
101
|
+
try {
|
|
102
|
+
handle = await open(join(home, ".claude.json"), constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
103
|
+
const info = await handle.stat();
|
|
104
|
+
if (!info.isFile() || info.size > 4194304 || info.mode & 0o022 || typeof process.getuid === "function" && info.uid !== process.getuid())
|
|
105
|
+
throw new Error("Unsafe Claude configuration");
|
|
106
|
+
const result = z.object({ mcpServers: z.record(z.string(), z.unknown()).optional() }).passthrough().parse(JSON.parse(await handle.readFile("utf8")));
|
|
107
|
+
return result.mcpServers?.[name] ?? null;
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
111
|
+
return null;
|
|
112
|
+
throw new Error("Claude MCP configuration could not be safely inspected. No server was overwritten.");
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
await handle?.close();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export async function registerAgent(registration, agent, env, home) {
|
|
119
|
+
if (await agentRegistration(registration.client, agent, env, home, registration.name))
|
|
120
|
+
throw new Error("A Relvo MCP entry appeared while setup was awaiting approval. It was preserved; rerun setup after resolving the conflict.");
|
|
121
|
+
const args = registration.client === "codex" ? ["mcp", "add", registration.name, "--", registration.command, ...registration.args]
|
|
122
|
+
: ["mcp", "add", "--transport", "stdio", "--scope", "user", registration.name, "--", registration.command, ...registration.args];
|
|
123
|
+
try {
|
|
124
|
+
await execute(agent, args, { cwd: home, env: cleanAgentEnvironment(env, home), timeout: 20000, maxBuffer: 1048576 });
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
throw new Error("Relvo MCP registration failed. Login was saved; rerun setup to resume without minting another key.");
|
|
128
|
+
}
|
|
129
|
+
if (!registrationMatches(await agentRegistration(registration.client, agent, env, home, registration.name), registration))
|
|
130
|
+
throw new Error("Relvo could not verify the saved MCP registration. Run doctor before using it.");
|
|
131
|
+
}
|
|
132
|
+
export async function probeMcp(registration, env, home, projectId, signal) {
|
|
133
|
+
const failed = new Error("Relvo MCP verification failed. Run doctor to inspect the saved connection; no paid work was started.");
|
|
134
|
+
if (signal?.aborted)
|
|
135
|
+
throw failed;
|
|
136
|
+
const child = spawn(registration.command, registration.args, { cwd: home, env: { ...cleanAgentEnvironment(env, home), [managedCliChildEnv]: "1", RELVO_PROJECT_AUTO_SYNC: "0" }, stdio: ["pipe", "pipe", "pipe"] });
|
|
137
|
+
let stderrBytes = 0, stdoutBytes = 0;
|
|
138
|
+
const aborted = new AbortController();
|
|
139
|
+
const cancel = () => aborted.abort(failed);
|
|
140
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
141
|
+
if (signal?.aborted)
|
|
142
|
+
cancel();
|
|
143
|
+
child.stderr.on("data", (chunk) => { stderrBytes += chunk.byteLength; if (stderrBytes > 65536)
|
|
144
|
+
aborted.abort(failed); });
|
|
145
|
+
child.stdout.on("data", (chunk) => { stdoutBytes += chunk.byteLength; if (stdoutBytes > 1048576)
|
|
146
|
+
aborted.abort(failed); });
|
|
147
|
+
child.on("error", () => aborted.abort(failed));
|
|
148
|
+
const deadline = setTimeout(() => aborted.abort(failed), 30000);
|
|
149
|
+
const lines = createInterface({ input: child.stdout })[Symbol.asyncIterator]();
|
|
150
|
+
let id = 0;
|
|
151
|
+
const meta = { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientInfo": { name: "relvo-doctor", version: "1" }, "io.modelcontextprotocol/clientCapabilities": {} };
|
|
152
|
+
const rpc = async (method, params) => {
|
|
153
|
+
const requestId = ++id;
|
|
154
|
+
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id: requestId, method, params: { ...params, _meta: meta } }) + "\n");
|
|
155
|
+
for (;;) {
|
|
156
|
+
aborted.signal.throwIfAborted();
|
|
157
|
+
const next = await new Promise((resolve, reject) => {
|
|
158
|
+
const cancel = () => reject(failed);
|
|
159
|
+
aborted.signal.addEventListener("abort", cancel, { once: true });
|
|
160
|
+
lines.next().then(resolve, reject).finally(() => aborted.signal.removeEventListener("abort", cancel));
|
|
161
|
+
});
|
|
162
|
+
if (next.done)
|
|
163
|
+
throw failed;
|
|
164
|
+
const reply = z.object({ id: z.number().optional(), result: z.record(z.string(), z.unknown()).optional(), error: z.unknown().optional() }).parse(JSON.parse(next.value));
|
|
165
|
+
if (reply.id === requestId) {
|
|
166
|
+
if (reply.error || !reply.result)
|
|
167
|
+
throw failed;
|
|
168
|
+
return reply.result;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
try {
|
|
173
|
+
await rpc("server/discover", {});
|
|
174
|
+
const catalog = z.object({ tools: z.array(z.object({ name: z.string() }).passthrough()) }).parse(await rpc("tools/list", {}));
|
|
175
|
+
const names = new Set(catalog.tools.map((tool) => tool.name));
|
|
176
|
+
const compact = ["platform_tools", "platform_run", "platform"].every((name) => names.has(name));
|
|
177
|
+
if (!compact && !["platform_project", "platform_tools", "platform_skills", "platform_quote", "platform_execute"].every((name) => names.has(name)))
|
|
178
|
+
throw failed;
|
|
179
|
+
const call = (name, input) => rpc("tools/call", compact ? { name: "platform", arguments: { operation: "call", input: { name, input } } } : { name, arguments: input });
|
|
180
|
+
const project = z.object({ isError: z.boolean().optional(), structuredContent: z.object({ data: z.object({ developerProjectId: z.string() }) }) }).parse(await call("platform_project", {}));
|
|
181
|
+
if (project.isError || project.structuredContent.data.developerProjectId !== projectId)
|
|
182
|
+
throw failed;
|
|
183
|
+
const guides = await call("platform_skills", { summary: true });
|
|
184
|
+
if (guides.isError || !guides.structuredContent)
|
|
185
|
+
throw failed;
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
throw failed;
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
signal?.removeEventListener("abort", cancel);
|
|
192
|
+
clearTimeout(deadline);
|
|
193
|
+
const closed = child.exitCode === null && child.signalCode === null ? once(child, "close") : null;
|
|
194
|
+
child.kill("SIGTERM");
|
|
195
|
+
const force = setTimeout(() => child.kill("SIGKILL"), 2000);
|
|
196
|
+
await closed;
|
|
197
|
+
clearTimeout(force);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
export const mcpWorkerEnv = "RELVO_MCP_WORKER";
|
|
4
|
+
const HANDSHAKE_ID = "relvo-supervisor-initialize";
|
|
5
|
+
const HANDSHAKE_TIMEOUT_MS = 120_000, EXIT_GRACE_MS = 5_000;
|
|
6
|
+
const messageSchema = z.object({ id: z.union([z.string(), z.number()]).nullable().optional(), method: z.string().optional() }).loose();
|
|
7
|
+
const serverInfoSchema = z.object({ version: z.string() }).loose().optional();
|
|
8
|
+
const handshakeSchema = z.object({ result: z.object({ serverInfo: serverInfoSchema, _meta: z.object({ "io.modelcontextprotocol/serverInfo": serverInfoSchema }).loose().optional(),
|
|
9
|
+
capabilities: z.object({ tools: z.object({ listChanged: z.boolean().optional() }).loose().optional() }).loose().optional() }).loose() }).loose();
|
|
10
|
+
function parsed(line) {
|
|
11
|
+
try {
|
|
12
|
+
return messageSchema.safeParse(JSON.parse(line)).data ?? null;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function superviseMcp({ input, output, start, target, version, stop, pollMs = 30_000, retryMs = 300_000, now = Date.now }) {
|
|
19
|
+
return new Promise(resolve => {
|
|
20
|
+
const fromHost = new Set(), fromServer = new Set();
|
|
21
|
+
const watch = (worker) => {
|
|
22
|
+
const live = { worker, onLine: () => { } };
|
|
23
|
+
createInterface({ input: worker.stdout, crlfDelay: Infinity }).on("line", line => { live.onLine(line); });
|
|
24
|
+
return live;
|
|
25
|
+
};
|
|
26
|
+
let opening = null, initialized = null;
|
|
27
|
+
let current = watch(start(false)), next = null;
|
|
28
|
+
let running = version, replacing = false, retryAt = 0, closing = false;
|
|
29
|
+
const send = (line) => { output.write(`${line}\n`); };
|
|
30
|
+
const finish = (code) => {
|
|
31
|
+
clearInterval(poll);
|
|
32
|
+
next?.live.worker.kill();
|
|
33
|
+
hostLines.close();
|
|
34
|
+
resolve(code);
|
|
35
|
+
};
|
|
36
|
+
const follow = (live) => { void live.worker.exited.then(code => { if (live === current)
|
|
37
|
+
finish(code); }); };
|
|
38
|
+
const close = () => {
|
|
39
|
+
if (closing)
|
|
40
|
+
return;
|
|
41
|
+
closing = true;
|
|
42
|
+
current.worker.stdin.end();
|
|
43
|
+
setTimeout(() => { current.worker.kill(); }, EXIT_GRACE_MS).unref();
|
|
44
|
+
};
|
|
45
|
+
const advance = () => {
|
|
46
|
+
if (!next || fromHost.size || fromServer.size)
|
|
47
|
+
return;
|
|
48
|
+
const previous = current, { live, version: adopted, toolsChanged } = next;
|
|
49
|
+
next = null;
|
|
50
|
+
current = live;
|
|
51
|
+
running = adopted;
|
|
52
|
+
live.onLine = serve;
|
|
53
|
+
previous.onLine = () => { };
|
|
54
|
+
follow(live);
|
|
55
|
+
previous.worker.stdin.end();
|
|
56
|
+
setTimeout(() => { previous.worker.kill(); }, EXIT_GRACE_MS).unref();
|
|
57
|
+
if (toolsChanged)
|
|
58
|
+
send(JSON.stringify({ jsonrpc: "2.0", method: "notifications/tools/list_changed" }));
|
|
59
|
+
};
|
|
60
|
+
const serve = (line) => {
|
|
61
|
+
const message = parsed(line), id = message?.id;
|
|
62
|
+
if (id !== undefined && id !== null) {
|
|
63
|
+
if (message?.method)
|
|
64
|
+
fromServer.add(id);
|
|
65
|
+
else
|
|
66
|
+
fromHost.delete(id);
|
|
67
|
+
}
|
|
68
|
+
send(line);
|
|
69
|
+
advance();
|
|
70
|
+
};
|
|
71
|
+
current.onLine = serve;
|
|
72
|
+
follow(current);
|
|
73
|
+
const handshake = (live) => new Promise(settle => {
|
|
74
|
+
if (!opening) {
|
|
75
|
+
settle({ toolsChanged: false });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const timeout = setTimeout(() => { settle(null); }, HANDSHAKE_TIMEOUT_MS);
|
|
79
|
+
timeout.unref();
|
|
80
|
+
void live.worker.exited.then(() => { clearTimeout(timeout); settle(null); });
|
|
81
|
+
live.onLine = line => {
|
|
82
|
+
if (parsed(line)?.id !== HANDSHAKE_ID)
|
|
83
|
+
return;
|
|
84
|
+
clearTimeout(timeout);
|
|
85
|
+
const reply = handshakeSchema.safeParse(JSON.parse(line)).data?.result;
|
|
86
|
+
if (!reply) {
|
|
87
|
+
settle(null);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (initialized)
|
|
91
|
+
live.worker.stdin.write(`${initialized}\n`);
|
|
92
|
+
settle({ version: (reply.serverInfo ?? reply._meta?.["io.modelcontextprotocol/serverInfo"])?.version, toolsChanged: reply.capabilities?.tools?.listChanged === true });
|
|
93
|
+
};
|
|
94
|
+
live.worker.stdin.write(`${JSON.stringify({ ...opening, id: HANDSHAKE_ID })}\n`);
|
|
95
|
+
});
|
|
96
|
+
const check = async () => {
|
|
97
|
+
if (closing || replacing || next || now() < retryAt)
|
|
98
|
+
return;
|
|
99
|
+
replacing = true;
|
|
100
|
+
try {
|
|
101
|
+
const wanted = await target().catch(() => null);
|
|
102
|
+
if (!wanted || wanted === running)
|
|
103
|
+
return;
|
|
104
|
+
retryAt = now() + retryMs;
|
|
105
|
+
const live = watch(start(true)), ready = await handshake(live);
|
|
106
|
+
if (!ready || closing || ready.version && ready.version !== wanted) {
|
|
107
|
+
live.worker.kill();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
next = { live, version: wanted, toolsChanged: ready.toolsChanged };
|
|
111
|
+
advance();
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
replacing = false;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const poll = setInterval(() => { void check(); }, pollMs);
|
|
118
|
+
poll.unref();
|
|
119
|
+
const hostLines = createInterface({ input, crlfDelay: Infinity }).on("line", line => {
|
|
120
|
+
const message = parsed(line), id = message?.id;
|
|
121
|
+
if (message?.method === "initialize" || message?.method === "server/discover")
|
|
122
|
+
opening = message;
|
|
123
|
+
if (message?.method === "notifications/initialized")
|
|
124
|
+
initialized = line;
|
|
125
|
+
if (id !== undefined && id !== null) {
|
|
126
|
+
if (message?.method)
|
|
127
|
+
fromHost.add(id);
|
|
128
|
+
else
|
|
129
|
+
fromServer.delete(id);
|
|
130
|
+
}
|
|
131
|
+
current.worker.stdin.write(`${line}\n`);
|
|
132
|
+
advance();
|
|
133
|
+
}).on("close", close);
|
|
134
|
+
stop?.addEventListener("abort", close, { once: true });
|
|
135
|
+
});
|
|
136
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { PLATFORM_DISTRIBUTIONS } from "../client/index.js";
|
|
2
|
+
import { runCli } from "./cli.js";
|
|
3
|
+
const COMMANDS = [
|
|
4
|
+
["project", "project", "Show the developer organization and project bound to the API key"],
|
|
5
|
+
["usage", "usage", "Show the current project's monthly request count and spend"],
|
|
6
|
+
["tools", `tools [--query <text>] [--category <id>] [--provider <id>]
|
|
7
|
+
[--available-only | --include-unavailable] [--summary | --with-schemas]
|
|
8
|
+
[--max-charge-micros <integer>] [--quote-arguments '<json>'] [--price-unit <unit>]
|
|
9
|
+
[--sort-by relevance|price_asc] [--include '<filter-json>'] [--exclude '<filter-json>']
|
|
10
|
+
[--limit <1-12>] [--mode auto|keyword|semantic]`, "Search up to 12 tool summaries and USD prices (default 8); search uses monthly free calls, then is charged"],
|
|
11
|
+
["tool", "tool <name>", "Inspect one tool's arguments, example and price"],
|
|
12
|
+
["executions", "executions [--limit <1-100>] [--before <id>] [--status <state>] [--tool <name>]", "Recover the API key's project execution history"],
|
|
13
|
+
["quote", "quote <tool> '<arguments-json>' [--provider <id>] [--relevance-check auto|light] [--maximum-charge-micros <cap>]", "Get a free quote without running the tool"],
|
|
14
|
+
["execute", `execute <tool> '<arguments-json>' --maximum-charge-micros <integer>
|
|
15
|
+
--price-version <version> --idempotency-key <key> [--provider <id>] [--relevance-check auto|light] [--raw]`, "Reserve up to the quoted amount and enqueue paid work"],
|
|
16
|
+
["execution", "execution <id> [--raw]", "Read status, a bounded output preview and the final USD charge"],
|
|
17
|
+
["result", `result <id> [--path '<JSON-array>'] [--fields '<JSON-array>'] [--offset <n>] [--limit <n>]
|
|
18
|
+
[--max-chars <1-8000>]`, "Read a selected page of the stored output without paying again"],
|
|
19
|
+
["wait", "wait <id> [--timeout <seconds>] [--raw]", "Wait for completion (default 480 seconds, maximum 900)"],
|
|
20
|
+
["cancel", "cancel <id> [--raw]", "Request cancellation of work that has not started"],
|
|
21
|
+
["artifact-url", "artifact-url <execution-id> <artifact-id>", "Get a private output's download URL, valid for up to 5 minutes"],
|
|
22
|
+
["download", "download <execution-id> <artifact-id> --out <file>", "Save an artifact after size/SHA-256 checks; never overwrite a file"],
|
|
23
|
+
["upload", "upload <file>", "Upload one explicit JPG/PNG/WebP/WAV/MP3/MP4/MOV input, at most 12 MiB"],
|
|
24
|
+
];
|
|
25
|
+
const REFUSED_OPTIONS = {
|
|
26
|
+
tools: ["--app", "--funding"],
|
|
27
|
+
quote: ["--session"],
|
|
28
|
+
execute: ["--session"],
|
|
29
|
+
result: ["--file", "--find", "--pick", "--wait"],
|
|
30
|
+
};
|
|
31
|
+
const entry = ([, usage, summary]) => ` ${usage}\n ${summary}`;
|
|
32
|
+
export const noimosPlatformHelp = `NoimosAI Developer Platform CLI
|
|
33
|
+
|
|
34
|
+
Usage: noimos-platform <command>
|
|
35
|
+
noimos-platform <command> --help | help <command>
|
|
36
|
+
|
|
37
|
+
${COMMANDS.map(entry).join("\n")}
|
|
38
|
+
|
|
39
|
+
Get a quote first. Use its price version and maximum charge (1 USD = 1000000
|
|
40
|
+
micros). Keep the same body and idempotency key when retrying a submission.
|
|
41
|
+
Execution is asynchronous. Poll at intervals of at least 5 seconds.
|
|
42
|
+
For quote and execute, use - in place of arguments-json to read JSON from stdin,
|
|
43
|
+
or --input-file <path> to read a JSON file (at most 1 MiB).
|
|
44
|
+
wait exits 0 on success, 1 on failed/cancelled/needs_review, 3 on timeout.
|
|
45
|
+
A timeout leaves accepted work running; read or wait on the same execution ID.
|
|
46
|
+
Large outputs stay in the execution: read them with result. --raw returns the
|
|
47
|
+
full output for execution, wait, execute or cancel.
|
|
48
|
+
|
|
49
|
+
Standalone tools only: no workspace, brand, knowledge base, dataset, connected
|
|
50
|
+
provider account, post, workspace skill or chat-session commands. Scope comes
|
|
51
|
+
from the developer API key; supply prompts, public URLs, project files or artifacts.
|
|
52
|
+
|
|
53
|
+
Environment: NOIMOS_PLATFORM_API_URL, NOIMOS_PLATFORM_API_KEY
|
|
54
|
+
Local emulators only: NOIMOS_PLATFORM_ENV=local
|
|
55
|
+
|
|
56
|
+
Manage organizations, members, keys and billing at platform.noimosai.com.
|
|
57
|
+
`;
|
|
58
|
+
export const NOIMOS_PLATFORM_CLI_SURFACE = {
|
|
59
|
+
distribution: PLATFORM_DISTRIBUTIONS.noimos,
|
|
60
|
+
help: noimosPlatformHelp,
|
|
61
|
+
fullHelp: noimosPlatformHelp,
|
|
62
|
+
commandHelp: name => {
|
|
63
|
+
const command = COMMANDS.find(([commandName]) => commandName === name);
|
|
64
|
+
return command && `Usage: noimos-platform ${command[1]}\n\n${command[2]}.\nEnvironment: NOIMOS_PLATFORM_API_URL, NOIMOS_PLATFORM_API_KEY\n`;
|
|
65
|
+
},
|
|
66
|
+
supports: ([name, ...rest]) => COMMANDS.some(([commandName]) => commandName === name) && (name !== "project" || rest.length === 0)
|
|
67
|
+
&& !(REFUSED_OPTIONS[name] ?? []).some(option => rest.some(arg => arg === option || arg.startsWith(`${option}=`))),
|
|
68
|
+
savedLogins: false,
|
|
69
|
+
};
|
|
70
|
+
export function runNoimosPlatformCli(args, env, output, error, transport = fetch, stdin = process.stdin) {
|
|
71
|
+
return runCli(args, env, output, error, transport, stdin, undefined, NOIMOS_PLATFORM_CLI_SURFACE);
|
|
72
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { lstat, rename } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { localProfilePath, profileDirectory, safeDirectory, writePrivateBytes, writePrivateJson } from "../client/local-profile.js";
|
|
5
|
+
async function destination(home, projectId, source) {
|
|
6
|
+
const profile = localProfilePath(home);
|
|
7
|
+
await profileDirectory(profile, true);
|
|
8
|
+
const root = join(dirname(profile), "project-archives");
|
|
9
|
+
await safeDirectory(root, true, true);
|
|
10
|
+
const folder = join(root, `${createHash("sha256").update(projectId).digest("hex").slice(0, 16)}-${randomUUID()}`);
|
|
11
|
+
await safeDirectory(folder, true, true);
|
|
12
|
+
await writePrivateJson(join(folder, "origin.json"), { projectId, source }, false, 16_384);
|
|
13
|
+
return join(folder, basename(source));
|
|
14
|
+
}
|
|
15
|
+
export async function archiveProjectPath(home, projectId, source) {
|
|
16
|
+
const before = await lstat(source);
|
|
17
|
+
if (before.isSymbolicLink() || !before.isFile() && !before.isDirectory() || typeof process.getuid === "function" && before.uid !== process.getuid())
|
|
18
|
+
throw new Error(`Cannot archive unsafe managed path: ${source}`);
|
|
19
|
+
const backup = await destination(home, projectId, source);
|
|
20
|
+
await rename(source, backup);
|
|
21
|
+
return backup;
|
|
22
|
+
}
|
|
23
|
+
export async function archiveProjectBytes(home, projectId, source, bytes) {
|
|
24
|
+
const backup = await destination(home, projectId, source);
|
|
25
|
+
await writePrivateBytes(backup, bytes, false, Math.max(bytes.byteLength, 1));
|
|
26
|
+
return backup;
|
|
27
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { localProfilePath } from "../client/local-profile.js";
|
|
4
|
+
import { syncTeamSkills } from "./team-skills.js";
|
|
5
|
+
import { TEAM_HARNESSES, assertHarnessProjectSwitch, harnessRoots, readCustomHarnesses, syncTeamHarness } from "./team-harness.js";
|
|
6
|
+
import { invalidateProjectSyncReceipts } from "./project-sync.js";
|
|
7
|
+
export async function deactivateProjectFiles(projectId, env = process.env, home = homedir()) {
|
|
8
|
+
home = await realpath(home);
|
|
9
|
+
const roots = harnessRoots(home, env, await readCustomHarnesses(home));
|
|
10
|
+
const clients = [...TEAM_HARNESSES, ...roots.custom.map(entry => entry.name)];
|
|
11
|
+
const empty = {
|
|
12
|
+
teamSkills: async () => ({ data: { skills: [] } }),
|
|
13
|
+
teamSkill: async () => { throw new Error("Project deactivation never downloads or installs a new item."); },
|
|
14
|
+
};
|
|
15
|
+
await assertHarnessProjectSwitch(roots, clients, projectId);
|
|
16
|
+
await invalidateProjectSyncReceipts(localProfilePath(home), projectId);
|
|
17
|
+
const skills = await syncTeamSkills(home, clients, projectId, empty, false, roots, true);
|
|
18
|
+
const harnesses = await syncTeamHarness(roots, clients, projectId, empty, false, env, true);
|
|
19
|
+
const removed = skills.flatMap(result => result.removed), archived = skills.flatMap(result => (result.archived ?? []).map(item => ({ path: item.folder, backup: item.backup })));
|
|
20
|
+
for (const result of Object.values(harnesses)) {
|
|
21
|
+
removed.push(...result.removed, ...result.folders?.removed ?? []);
|
|
22
|
+
archived.push(...result.archived ?? [], ...result.folders?.archived ?? []);
|
|
23
|
+
}
|
|
24
|
+
return { projectId, removed, archived, reloadRequired: true };
|
|
25
|
+
}
|