@noimosai/platform-mcp 0.1.3 → 0.1.5
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 +17 -48
- package/bin/noimos-platform-mcp.mjs +4 -5
- package/dist/cli/cli.js +565 -0
- package/dist/cli/command-help.js +395 -0
- package/dist/cli/download.js +79 -0
- package/dist/cli/index.js +3 -0
- 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 +265 -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 +911 -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/instructions.js +26 -0
- package/dist/mcp/local-projects.js +42 -0
- package/dist/mcp/local-upload.js +7 -13
- package/dist/mcp/noimos-runtime.js +18 -0
- package/dist/mcp/noimos-server.js +38 -0
- package/dist/mcp/operations.js +41 -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 +65 -11
- package/dist/mcp/server.js +382 -40
- 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 +5 -3
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { realpath, unlink } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { PlatformClient } from "../client/index.js";
|
|
7
|
+
import { localProfilePath, readLocalProfile, readPrivateJson, writePrivateJson, runtimeConfig } from "../client/local-profile.js";
|
|
8
|
+
import { localProjectProfile } from "../client/local-projects.js";
|
|
9
|
+
import { platformTeamSkillSummarySchema, teamItemTargets, teamSkillKindNeedsAdmin } from "../schema/platformTeamSkills.js";
|
|
10
|
+
import { withProfileLock } from "./setup-lock.js";
|
|
11
|
+
import { syncTeamSkills, teamSkillsUseScripts } from "./team-skills.js";
|
|
12
|
+
import { TEAM_HARNESSES, assertHarnessProjectSwitch, harnessRoots, harnessesPresent, readCustomHarnesses, syncTeamHarness, teamHarnessUsesScripts } from "./team-harness.js";
|
|
13
|
+
import { harnessView } from "./team-harness-targets.js";
|
|
14
|
+
const metadataSchema = platformTeamSkillSummarySchema.pick({ slug: true, name: true, kind: true, adminOnly: true, harnesses: true, version: true });
|
|
15
|
+
const sha = z.string().regex(/^[a-f0-9]{64}$/);
|
|
16
|
+
const receiptSchema = z.object({ client: z.string().min(1).max(32), projectId: z.string().min(1).max(256), roots: sha,
|
|
17
|
+
items: z.array(metadataSchema).max(10000), scriptsAllowed: z.boolean() }).strict();
|
|
18
|
+
const stateSchema = z.object({ managedBy: z.literal("relvo-cli"), version: z.literal(1), installationId: z.string(), apiUrl: z.string(), consoleUrl: z.string(), local: z.boolean(),
|
|
19
|
+
enabled: z.boolean(), receipts: z.array(receiptSchema).max(32), dismissed: z.array(sha).max(50) }).strict();
|
|
20
|
+
const statePath = (path) => join(dirname(path), "project-sync.json");
|
|
21
|
+
const maximumBytes = 32 * 1024 * 1024;
|
|
22
|
+
const digest = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
23
|
+
const metadata = (item) => {
|
|
24
|
+
const parsed = metadataSchema.parse(item);
|
|
25
|
+
return { ...parsed, harnesses: [...parsed.harnesses].sort() };
|
|
26
|
+
};
|
|
27
|
+
const missing = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
28
|
+
const binding = (profile) => ({ installationId: profile.installationId, apiUrl: profile.apiUrl, consoleUrl: profile.consoleUrl, local: profile.local });
|
|
29
|
+
async function readState(path, profile) {
|
|
30
|
+
try {
|
|
31
|
+
const saved = stateSchema.parse(await readPrivateJson(statePath(path), maximumBytes));
|
|
32
|
+
const sameApi = saved.apiUrl === profile.apiUrl || !saved.local && !profile.local && /(^|\.)relvo\.si$/.test(new URL(profile.apiUrl).hostname);
|
|
33
|
+
if (saved.installationId !== profile.installationId || !sameApi || saved.consoleUrl !== profile.consoleUrl || saved.local !== profile.local)
|
|
34
|
+
throw new Error("Different installation");
|
|
35
|
+
if (new Set(saved.receipts.map(receipt => receipt.client)).size !== saved.receipts.length)
|
|
36
|
+
throw new Error("Duplicate targets");
|
|
37
|
+
return { ...saved, apiUrl: profile.apiUrl };
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (missing(error))
|
|
41
|
+
return { managedBy: "relvo-cli", version: 1, ...binding(profile), enabled: true, receipts: [], dismissed: [] };
|
|
42
|
+
throw new Error("Relvo project sync state is unsafe, unreadable or belongs to another installation. No project files were changed.");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const writeState = (path, state) => writePrivateJson(statePath(path), stateSchema.parse(state), true, maximumBytes);
|
|
46
|
+
export async function removeOwnedProjectSyncState(path, installationId) {
|
|
47
|
+
try {
|
|
48
|
+
const saved = stateSchema.parse(await readPrivateJson(statePath(path), maximumBytes));
|
|
49
|
+
if (saved.installationId !== installationId)
|
|
50
|
+
return "preserved";
|
|
51
|
+
await unlink(statePath(path));
|
|
52
|
+
return "removed";
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
return missing(error) ? "missing" : "preserved";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export async function invalidateProjectSyncReceipts(path, targetProjectId) {
|
|
59
|
+
const profile = await readLocalProfile(path);
|
|
60
|
+
if (!profile || profile.state !== "ready")
|
|
61
|
+
return;
|
|
62
|
+
const state = await readState(path, profile);
|
|
63
|
+
const receipts = state.receipts.filter(receipt => receipt.projectId === targetProjectId);
|
|
64
|
+
if (receipts.length !== state.receipts.length)
|
|
65
|
+
await writeState(path, { ...state, receipts });
|
|
66
|
+
}
|
|
67
|
+
async function targets(roots, profile, selected = "all") {
|
|
68
|
+
const names = [...TEAM_HARNESSES, ...roots.custom.map(entry => entry.name)];
|
|
69
|
+
const clients = selected === "all" ? [...new Set([
|
|
70
|
+
...profile.registrations.length ? profile.registrations.map(entry => entry.client) : ["claude", "codex"],
|
|
71
|
+
...(await harnessesPresent(roots)).filter(name => name !== "claude" && name !== "codex"),
|
|
72
|
+
])] : [...new Set(selected.split(",").map(name => name.trim()))];
|
|
73
|
+
const unknown = clients.filter(name => !names.includes(name));
|
|
74
|
+
if (!clients.length || unknown.length)
|
|
75
|
+
throw new Error(`Unknown agent ${unknown.join(", ") || "(none)"}. Use all or a comma list of ${names.join(", ")}.`);
|
|
76
|
+
return clients.sort();
|
|
77
|
+
}
|
|
78
|
+
const rootDigest = (roots, name) => {
|
|
79
|
+
const view = harnessView(roots, name);
|
|
80
|
+
return digest({ like: view.like, dir: view.roots.dirs[view.like], agents: view.roots.agents[view.like] });
|
|
81
|
+
};
|
|
82
|
+
const forTarget = (items, roots, name) => items.filter(item => teamItemTargets(item.harnesses, harnessView(roots, name).like));
|
|
83
|
+
function reviewedReader(client, items) {
|
|
84
|
+
return {
|
|
85
|
+
teamSkills: async (kind) => ({ data: { skills: kind === "all" ? items : items.filter(item => item.kind === (kind ?? "skill")) } }),
|
|
86
|
+
teamSkill: async (slug) => {
|
|
87
|
+
const wanted = items.find(item => item.slug === slug);
|
|
88
|
+
if (!wanted)
|
|
89
|
+
throw new Error("This item was not in the approved snapshot. Check changes again before syncing.");
|
|
90
|
+
const value = await client.teamSkill(slug);
|
|
91
|
+
if (digest(metadata(value.data.skill)) !== digest(metadata(wanted)))
|
|
92
|
+
throw new Error("A project item changed during sync. Some earlier files may have been synced; check changes again and approve the new snapshot before retrying.");
|
|
93
|
+
return value;
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
async function synchronize(path, profile, roots, clients, client, items, allowScripts, state, env) {
|
|
98
|
+
const projectId = profile.grant.developerProjectId, reader = reviewedReader(client, items);
|
|
99
|
+
await assertHarnessProjectSwitch(roots, clients, projectId);
|
|
100
|
+
const skills = await syncTeamSkills(roots.home, clients, projectId, reader, allowScripts, roots);
|
|
101
|
+
const harness = await syncTeamHarness(roots, clients, projectId, reader, allowScripts, env);
|
|
102
|
+
const results = clients.map(name => {
|
|
103
|
+
const shared = skills.find(result => !result.root && (result.client === name || result.alsoFor?.includes(name)));
|
|
104
|
+
const own = skills.find(result => result.root && result.client === name), ownSkills = own ? { ownSkills: own } : {};
|
|
105
|
+
if (!shared)
|
|
106
|
+
return { client: name, ...ownSkills, harness: harness[name] };
|
|
107
|
+
return shared.client === name ? { ...shared, ...ownSkills, harness: harness[name] } : { client: name, skillsSharedWith: shared.client, ...ownSkills, harness: harness[name] };
|
|
108
|
+
});
|
|
109
|
+
const receipts = clients.map(name => ({ client: name, projectId, roots: rootDigest(roots, name), items: forTarget(items.map(metadata), roots, name), scriptsAllowed: allowScripts }));
|
|
110
|
+
const configured = [...TEAM_HARNESSES, ...roots.custom.map(entry => entry.name)];
|
|
111
|
+
await writeState(path, { ...state, receipts: [...state.receipts.filter(receipt => !clients.includes(receipt.client) && configured.includes(receipt.client)), ...receipts] });
|
|
112
|
+
const all = [...skills, ...Object.values(harness), ...Object.values(harness).flatMap(result => result.folders ? [result.folders] : [])];
|
|
113
|
+
const kept = [...skills.flatMap(result => result.kept.map(entry => ({ path: entry.folder, reason: entry.reason }))),
|
|
114
|
+
...Object.values(harness).flatMap(result => [...result.kept, ...result.folders?.kept ?? []])];
|
|
115
|
+
const archived = [...skills.flatMap(result => (result.archived ?? []).map(entry => ({ path: entry.folder, backup: entry.backup }))),
|
|
116
|
+
...Object.values(harness).flatMap(result => [...result.archived ?? [], ...result.folders?.archived ?? []])];
|
|
117
|
+
const notSupported = Object.values(harness).flatMap(result => result.notSupported ?? []);
|
|
118
|
+
return { results, summary: { projectId, clients, written: all.reduce((sum, result) => sum + result.written.length, 0), unchanged: all.reduce((sum, result) => sum + result.unchanged.length, 0),
|
|
119
|
+
removed: all.reduce((sum, result) => sum + result.removed.length, 0), keptCount: kept.length, kept: kept.slice(0, 10),
|
|
120
|
+
archivedCount: archived.length, archived: archived.slice(0, 10), archiveDirectory: join(dirname(path), "project-archives"),
|
|
121
|
+
notSupportedCount: notSupported.length, notSupported: notSupported.slice(0, 10),
|
|
122
|
+
scriptsWithheld: skills.flatMap(result => [...result.scriptsNotExecutable ?? [], ...result.settingsWithheld ?? []]).length
|
|
123
|
+
+ Object.values(harness).reduce((sum, result) => sum + (result.settingsWithheld?.length ?? 0) + (result.notInstalled?.length ?? 0) + (result.folders?.scriptsNotExecutable?.length ?? 0), 0),
|
|
124
|
+
detailsTruncated: kept.length > 10 || archived.length > 10 || notSupported.length > 10, reloadRequired: true,
|
|
125
|
+
next: "Reload your agent's skills/settings or start a fresh session. Kept edits, unsupported items and withheld settings were not replaced. Archives remain in archiveDirectory." } };
|
|
126
|
+
}
|
|
127
|
+
async function context(path, env, options = {}) {
|
|
128
|
+
const home = await realpath(options.home ?? homedir()), now = options.now ?? Date.now;
|
|
129
|
+
if (path !== localProfilePath(home))
|
|
130
|
+
throw new Error("Use the managed profile belonging to this home directory before syncing files.");
|
|
131
|
+
let profile = await readLocalProfile(path);
|
|
132
|
+
if (!profile || profile.state !== "ready" || profile.pendingAck)
|
|
133
|
+
throw new Error("Complete or renew relvo setup before syncing project files.");
|
|
134
|
+
if (options.projectId)
|
|
135
|
+
profile = await localProjectProfile(path, options.projectId, env, options.transport, now());
|
|
136
|
+
else
|
|
137
|
+
await runtimeConfig(env, "cli", path, true);
|
|
138
|
+
if (Date.parse(profile.grant.expiresAt) <= now())
|
|
139
|
+
throw new Error("Complete or renew relvo setup before syncing project files.");
|
|
140
|
+
const roots = harnessRoots(home, env, await readCustomHarnesses(home));
|
|
141
|
+
return { profile, roots, clients: await targets(roots, profile, options.selected), state: await readState(path, profile) };
|
|
142
|
+
}
|
|
143
|
+
async function inspect(client, profile, verifyProject = true) {
|
|
144
|
+
if (verifyProject) {
|
|
145
|
+
const { data } = await client.project();
|
|
146
|
+
if (data.developerProjectId !== profile.grant.developerProjectId || data.developerOrganizationId !== profile.grant.developerOrganizationId)
|
|
147
|
+
throw new Error("The saved login does not match this project. No project files were changed.");
|
|
148
|
+
}
|
|
149
|
+
const items = (await client.teamSkills("all")).data.skills.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
150
|
+
if (new Set(items.map(item => item.slug)).size !== items.length)
|
|
151
|
+
throw new Error("The project returned duplicate items. No project files were changed.");
|
|
152
|
+
return items;
|
|
153
|
+
}
|
|
154
|
+
export async function syncProjectFiles(path, env, options = {}) {
|
|
155
|
+
return withProfileLock(path, async () => {
|
|
156
|
+
const { profile, roots, clients, state } = await context(path, env, options);
|
|
157
|
+
const client = new PlatformClient({ apiUrl: profile.apiUrl, apiKey: profile.grant.apiKey, local: profile.local, source: "cli" }, options.transport);
|
|
158
|
+
const items = await inspect(client, profile);
|
|
159
|
+
return (await synchronize(path, profile, roots, clients, client, items, options.allowScripts === true, state, env)).results;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function review(profile, roots, clients, items, state, scriptsPreviouslyAllowed) {
|
|
163
|
+
const projectId = profile.grant.developerProjectId, changes = [];
|
|
164
|
+
for (const name of clients) {
|
|
165
|
+
const saved = state.receipts.find(receipt => receipt.client === name && receipt.projectId === projectId && receipt.roots === rootDigest(roots, name));
|
|
166
|
+
const previous = saved?.items ?? [], current = forTarget(items, roots, name);
|
|
167
|
+
for (const item of current) {
|
|
168
|
+
const before = previous.find(entry => entry.slug === item.slug);
|
|
169
|
+
if (!before || digest(before) !== digest(item))
|
|
170
|
+
changes.push({ slug: item.slug, kind: item.kind, version: item.version, requiresScriptApproval: item.adminOnly || teamSkillKindNeedsAdmin(item.kind), change: before ? "updated" : "added", clients: [name] });
|
|
171
|
+
}
|
|
172
|
+
for (const item of previous)
|
|
173
|
+
if (!current.some(entry => entry.slug === item.slug))
|
|
174
|
+
changes.push({ slug: item.slug, kind: item.kind, version: item.version, requiresScriptApproval: item.adminOnly || teamSkillKindNeedsAdmin(item.kind), change: "removed", clients: [name] });
|
|
175
|
+
}
|
|
176
|
+
const combined = new Map();
|
|
177
|
+
for (const change of changes) {
|
|
178
|
+
const key = `${change.slug}:${change.change}:${change.version}`, previous = combined.get(key);
|
|
179
|
+
if (previous)
|
|
180
|
+
previous.clients.push(...change.clients);
|
|
181
|
+
else
|
|
182
|
+
combined.set(key, change);
|
|
183
|
+
}
|
|
184
|
+
const receipts = clients.map(name => state.receipts.find(receipt => receipt.client === name) ?? null);
|
|
185
|
+
const snapshot = digest({ ...binding(profile), projectId, organizationId: profile.grant.developerOrganizationId, clients,
|
|
186
|
+
roots: clients.map(name => rootDigest(roots, name)), receipts, items, scriptsPreviouslyAllowed });
|
|
187
|
+
return { projectId, enabled: state.enabled, clients, snapshot, changes: [...combined.values()], dismissed: state.dismissed.includes(snapshot),
|
|
188
|
+
scriptsPreviouslyAllowed };
|
|
189
|
+
}
|
|
190
|
+
export function localProjectSync(path, env, options = {}) {
|
|
191
|
+
const prepare = async () => {
|
|
192
|
+
const ctx = await context(path, env, options);
|
|
193
|
+
if (options.connected && ctx.profile.grant.developerProjectId !== options.connected.projectId)
|
|
194
|
+
throw new Error("The selected project changed. Reconnect MCP before reviewing or importing local project files.");
|
|
195
|
+
if (!options.selected && options.connected) {
|
|
196
|
+
const host = options.connected.clientName()?.toLowerCase() ?? "";
|
|
197
|
+
const name = [...TEAM_HARNESSES, ...ctx.roots.custom.map(entry => entry.name)].find(candidate => host === candidate || host.startsWith(`${candidate}-`) || host.startsWith(`${candidate}_`));
|
|
198
|
+
if (name)
|
|
199
|
+
ctx.clients = await targets(ctx.roots, ctx.profile, name);
|
|
200
|
+
}
|
|
201
|
+
const client = options.connected?.client ?? new PlatformClient({ apiUrl: ctx.profile.apiUrl, apiKey: ctx.profile.grant.apiKey, local: ctx.profile.local, source: "cli" }, options.transport);
|
|
202
|
+
return { ...ctx, client };
|
|
203
|
+
};
|
|
204
|
+
let polledLogin;
|
|
205
|
+
const capture = async (ctx, poll = false) => {
|
|
206
|
+
const login = `${ctx.profile.grant.apiKey}\n${ctx.profile.grant.developerOrganizationId}\n${ctx.profile.grant.developerProjectId}`;
|
|
207
|
+
const items = (await inspect(ctx.client, ctx.profile, !poll || polledLogin !== login)).filter(item => ctx.clients.some(name => teamItemTargets(item.harnesses, harnessView(ctx.roots, name).like)));
|
|
208
|
+
if (poll)
|
|
209
|
+
polledLogin = login;
|
|
210
|
+
const scriptsPreviouslyAllowed = await teamSkillsUseScripts(ctx.roots, ctx.clients, ctx.profile.grant.developerProjectId)
|
|
211
|
+
|| await teamHarnessUsesScripts(ctx.roots, ctx.clients, ctx.profile.grant.developerProjectId);
|
|
212
|
+
const current = await readLocalProfile(path);
|
|
213
|
+
if (!current || current.state !== "ready" || current.pendingAck || current.installationId !== ctx.profile.installationId || current.apiUrl !== ctx.profile.apiUrl
|
|
214
|
+
|| current.consoleUrl !== ctx.profile.consoleUrl || current.local !== ctx.profile.local || current.grant.apiKey !== ctx.profile.grant.apiKey
|
|
215
|
+
|| current.grant.developerProjectId !== ctx.profile.grant.developerProjectId || current.grant.developerOrganizationId !== ctx.profile.grant.developerOrganizationId)
|
|
216
|
+
throw new Error("The selected project changed during review. Reconnect MCP and check again before importing.");
|
|
217
|
+
return { items, review: review(ctx.profile, ctx.roots, ctx.clients, items.map(metadata), ctx.state, scriptsPreviouslyAllowed) };
|
|
218
|
+
};
|
|
219
|
+
const confirmed = (value) => { if (value !== true)
|
|
220
|
+
throw new Error("Ask the user for explicit approval before changing local project files or notification settings, then pass confirmedByUser:true (--yes on the CLI)."); };
|
|
221
|
+
const approvedSnapshot = (snapshot, current) => {
|
|
222
|
+
if (!sha.safeParse(snapshot).success || snapshot !== current.snapshot)
|
|
223
|
+
throw new Error("The reviewed project, versions or local targets changed. Check again and obtain approval of the new snapshot; no files were changed.");
|
|
224
|
+
};
|
|
225
|
+
return {
|
|
226
|
+
check: async () => (await capture(await prepare())).review,
|
|
227
|
+
poll: async () => {
|
|
228
|
+
if (env.RELVO_PROJECT_AUTO_SYNC === "0")
|
|
229
|
+
return undefined;
|
|
230
|
+
const ctx = await prepare();
|
|
231
|
+
return !ctx.state.enabled ? undefined : (await capture(ctx, true)).review;
|
|
232
|
+
},
|
|
233
|
+
apply: (snapshot, confirmedByUser, allowScripts, removeExecutableSettings = false) => withProfileLock(path, async () => {
|
|
234
|
+
confirmed(confirmedByUser);
|
|
235
|
+
if (allowScripts && removeExecutableSettings)
|
|
236
|
+
throw new Error("Choose whether to allow or remove executable settings, not both. No files were changed.");
|
|
237
|
+
const ctx = await prepare(), captured = await capture(ctx);
|
|
238
|
+
approvedSnapshot(snapshot, captured.review);
|
|
239
|
+
if (captured.review.scriptsPreviouslyAllowed && !allowScripts && !removeExecutableSettings)
|
|
240
|
+
throw new Error("This target was previously synced with executable settings. Read contents with platform operation:team_skill input:{slug} (full: platform_team_skill; CLI: relvo team-skills get <slug>), then separately approve allowScripts (--allow-scripts) to keep/update them or removeExecutableSettings (--remove-executable-settings) to remove the opt-in. No files were changed.");
|
|
241
|
+
return (await synchronize(path, ctx.profile, ctx.roots, ctx.clients, ctx.client, captured.items, allowScripts, ctx.state, env)).summary;
|
|
242
|
+
}),
|
|
243
|
+
dismiss: (snapshot, confirmedByUser) => withProfileLock(path, async () => {
|
|
244
|
+
confirmed(confirmedByUser);
|
|
245
|
+
const ctx = await prepare(), captured = await capture(ctx);
|
|
246
|
+
approvedSnapshot(snapshot, captured.review);
|
|
247
|
+
await writeState(path, { ...ctx.state, dismissed: [...ctx.state.dismissed.filter(value => value !== snapshot), snapshot].slice(-50) });
|
|
248
|
+
return { projectId: captured.review.projectId, snapshot, dismissed: true };
|
|
249
|
+
}),
|
|
250
|
+
configure: (enabled, confirmedByUser) => withProfileLock(path, async () => {
|
|
251
|
+
confirmed(confirmedByUser);
|
|
252
|
+
const ctx = await prepare();
|
|
253
|
+
await writeState(path, { ...ctx.state, enabled });
|
|
254
|
+
return { enabled, environmentDisabled: env.RELVO_PROJECT_AUTO_SYNC === "0", mode: "notify-and-ask", intervalSeconds: 60 };
|
|
255
|
+
}),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
export const projectSyncFactory = (env = process.env, home = homedir()) => connected => localProjectSync(connected.path, env, { home, connected });
|
|
259
|
+
export async function projectSyncStatus(path, env, home) {
|
|
260
|
+
return withProfileLock(path, async () => {
|
|
261
|
+
const ctx = await context(path, env, { home });
|
|
262
|
+
return { enabled: ctx.state.enabled, environmentDisabled: env.RELVO_PROJECT_AUTO_SYNC === "0", projectId: ctx.profile.grant.developerProjectId,
|
|
263
|
+
mode: "notify-and-ask", intervalSeconds: 60, reviewedClients: ctx.state.receipts.map(({ client, projectId }) => ({ client, projectId })) };
|
|
264
|
+
});
|
|
265
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { withLocalProfileLock as withProfileLock } from "../client/local-projects.js";
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { lstat, realpath } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { parseArgs, promisify } from "node:util";
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { randomBytes } from "node:crypto";
|
|
7
|
+
import { PLATFORM_CLI_VERSION } from "./version.js";
|
|
8
|
+
import { CliInputError } from "./input-error.js";
|
|
9
|
+
import { PlatformClient } from "../client/index.js";
|
|
10
|
+
import { LocalSetupClient, relvoApiUrl, verificationUrl } from "../client/local-setup.js";
|
|
11
|
+
import { localProfilePath, readLocalProfile, readLocalInstallation, removeUnreadableProfile, windowsLocalSetupRefusal, writeLocalProfile, removeLocalProfile, runtimeConfig } from "../client/local-profile.js";
|
|
12
|
+
import { completeProjectSetup, listLocalProjects, markProjectSetup, projectSetupInProgress, rememberLocalProject, removeLocalProjects, useLocalProject } from "../client/local-projects.js";
|
|
13
|
+
import { agentNames, agentRegistration, preflightAgent, preflightAgents, registrationMatches, registerAgent, probeMcp, executable } from "./local-agents.js";
|
|
14
|
+
import { installSkills, verifiedSkillBundle } from "./skills.js";
|
|
15
|
+
import { pushTeamSkill } from "./team-skills.js";
|
|
16
|
+
import { TEAM_HARNESSES, harnessKindOf, harnessRoots, pushTeamHarnessFile, readCustomHarnesses } from "./team-harness.js";
|
|
17
|
+
import { withProfileLock } from "./setup-lock.js";
|
|
18
|
+
import { removeOwnedUpdateState } from "./update-bootstrap.js";
|
|
19
|
+
import { deactivateProjectFiles } from "./project-files.js";
|
|
20
|
+
import { localProjectSync, projectSyncStatus, removeOwnedProjectSyncState, syncProjectFiles } from "./project-sync.js";
|
|
21
|
+
const execute = promisify(execFile);
|
|
22
|
+
const onRelvoDomain = (url) => /(^|\.)relvo\.si$/.test(new URL(url).hostname);
|
|
23
|
+
async function browser(url, env) {
|
|
24
|
+
const command = process.platform === "darwin" ? "/usr/bin/open" : await executable("xdg-open", env);
|
|
25
|
+
try {
|
|
26
|
+
await execute(command, [url], { timeout: 10000, maxBuffer: 65536 });
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
throw new Error("Open the verification URL printed above in your browser.");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function choice(name, options, value) {
|
|
33
|
+
const chosen = options.find(option => option === value);
|
|
34
|
+
if (chosen === undefined)
|
|
35
|
+
throw new CliInputError(`--${name} takes ${options.join(", ")}`);
|
|
36
|
+
return chosen;
|
|
37
|
+
}
|
|
38
|
+
function uniqueOptions(tokens) {
|
|
39
|
+
const names = tokens.filter((token) => token.kind === "option").map((token) => token.name);
|
|
40
|
+
if (new Set(names).size !== names.length)
|
|
41
|
+
throw new Error("Duplicate local setup option.");
|
|
42
|
+
}
|
|
43
|
+
function clientFor(profile, transport) { return new PlatformClient({ apiUrl: profile.apiUrl, apiKey: profile.grant.apiKey, local: profile.local, source: "cli" }, transport); }
|
|
44
|
+
async function inspectLogin(profile, transport, now) {
|
|
45
|
+
if (profile.pendingAck || Date.parse(profile.grant.expiresAt) <= now())
|
|
46
|
+
throw new Error("The saved login is incomplete or expired. Run setup to resume, or logout before starting another login.");
|
|
47
|
+
const client = clientFor(profile, transport), { data } = await client.project();
|
|
48
|
+
if (data.developerOrganizationId !== profile.grant.developerOrganizationId || data.developerProjectId !== profile.grant.developerProjectId)
|
|
49
|
+
throw new Error("The saved login does not match its approved project. No configuration was changed.");
|
|
50
|
+
await client.connections({ limit: 1 });
|
|
51
|
+
return client;
|
|
52
|
+
}
|
|
53
|
+
async function acknowledgeProfile(profile, setup, transport, now, path, deactivate) {
|
|
54
|
+
if (!profile.pendingAck)
|
|
55
|
+
return profile;
|
|
56
|
+
let ack;
|
|
57
|
+
try {
|
|
58
|
+
ack = await setup.acknowledge(profile.pendingAck);
|
|
59
|
+
}
|
|
60
|
+
catch (failure) {
|
|
61
|
+
if (now() <= Date.parse(profile.grant.expiresAt))
|
|
62
|
+
throw failure;
|
|
63
|
+
const observed = (await clientFor(profile, transport).project()).data;
|
|
64
|
+
if (observed.developerOrganizationId !== profile.grant.developerOrganizationId || observed.developerProjectId !== profile.grant.developerProjectId)
|
|
65
|
+
throw new Error("The saved approval could not be recovered for its original project.");
|
|
66
|
+
const { apiKey, status, ...metadata } = profile.grant;
|
|
67
|
+
void apiKey;
|
|
68
|
+
void status;
|
|
69
|
+
ack = { ...metadata, status: "acknowledged", expiresAt: metadata.approvedExpiresAt };
|
|
70
|
+
}
|
|
71
|
+
if (ack.apiKeyId !== profile.grant.apiKeyId || ack.developerProjectId !== profile.grant.developerProjectId || ack.developerOrganizationId !== profile.grant.developerOrganizationId ||
|
|
72
|
+
ack.approvedExpiresAt !== profile.grant.approvedExpiresAt || ack.expiresAt !== profile.grant.approvedExpiresAt || JSON.stringify([...ack.scopes].sort()) !== JSON.stringify([...profile.grant.scopes].sort()))
|
|
73
|
+
throw new Error("Relvo acknowledgement did not match the saved approval.");
|
|
74
|
+
const { pendingAck, ...saved } = profile;
|
|
75
|
+
void pendingAck;
|
|
76
|
+
const ready = { ...saved, grant: { ...saved.grant, expiresAt: ack.expiresAt, approvedExpiresAt: ack.approvedExpiresAt } };
|
|
77
|
+
const observed = (await clientFor(ready, transport).project()).data;
|
|
78
|
+
if (observed.developerOrganizationId !== ready.grant.developerOrganizationId || observed.developerProjectId !== ready.grant.developerProjectId)
|
|
79
|
+
throw new Error("The saved login does not match its approved project. No configuration was changed.");
|
|
80
|
+
await deactivate(ready.grant.developerProjectId);
|
|
81
|
+
await writeLocalProfile(path, ready);
|
|
82
|
+
return ready;
|
|
83
|
+
}
|
|
84
|
+
export async function runLocalCommand(args, env, output, transport, options = {}) {
|
|
85
|
+
if (process.platform === "win32")
|
|
86
|
+
throw new Error(windowsLocalSetupRefusal);
|
|
87
|
+
const home = await realpath(options.home ?? homedir()), path = localProfilePath(home), now = options.now ?? Date.now;
|
|
88
|
+
const deactivate = (id) => deactivateProjectFiles(id, env, home);
|
|
89
|
+
const wait = options.wait ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)));
|
|
90
|
+
if (args[0] === "project") {
|
|
91
|
+
if (args.length === 2 && args[1] === "list") {
|
|
92
|
+
output(JSON.stringify({ projects: await listLocalProjects(path, now()) }));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (args.length === 3 && args[1] === "use" && args[2].trim() && !args[2].startsWith("-")) {
|
|
96
|
+
const project = await useLocalProject(path, args[2], env, "cli", transport, now(), deactivate);
|
|
97
|
+
output(JSON.stringify({ project, restartMcp: true, next: "Previous managed project files were deactivated. Reconnect Relvo MCP and start a fresh agent session to avoid old project instructions. Run relvo team-skills sync when you approve importing the new project's files; executable files still require --allow-scripts." }));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (args[1] === "add") {
|
|
101
|
+
const { values, tokens } = parseArgs({ args: args.slice(2), tokens: true, options: { project: { type: "string" }, "no-browser": { type: "boolean" } } });
|
|
102
|
+
uniqueOptions(tokens);
|
|
103
|
+
if (values.project !== undefined && !values.project.trim())
|
|
104
|
+
throw new Error("Provide a project ID after --project.");
|
|
105
|
+
const saved = await readLocalProfile(path);
|
|
106
|
+
if (!saved)
|
|
107
|
+
throw new Error("Run relvo setup with your API and console URLs before adding another project.");
|
|
108
|
+
return runLocalCommand(["setup", "--api-url", saved.apiUrl, "--console-url", saved.consoleUrl, "--client", "terminal", "--new-project",
|
|
109
|
+
...(values.project ? ["--project", values.project.trim()] : []), ...(saved.local ? ["--local"] : []), ...(values["no-browser"] ? ["--no-browser"] : [])], env, output, transport, options);
|
|
110
|
+
}
|
|
111
|
+
throw new Error("Usage: relvo project list | add [--project <project-id>] [--no-browser] | use <project-id>");
|
|
112
|
+
}
|
|
113
|
+
if (args[0] === "setup" || args[0] === "login") {
|
|
114
|
+
const { values, tokens } = parseArgs({ args: args.slice(1), tokens: true, options: { "api-url": { type: "string" }, "console-url": { type: "string" }, client: { type: "string" }, "mcp-runtime": { type: "string" }, local: { type: "boolean" }, "no-browser": { type: "boolean" }, "new-project": { type: "boolean" }, project: { type: "string" } } });
|
|
115
|
+
uniqueOptions(tokens);
|
|
116
|
+
const requested = choice("client", ["auto", "claude", "codex", "terminal"], values.client ?? "auto");
|
|
117
|
+
const runtime = choice("mcp-runtime", ["registry", "installed"], values["mcp-runtime"] ?? "registry");
|
|
118
|
+
let installedEntrypoint;
|
|
119
|
+
if (runtime === "installed") {
|
|
120
|
+
const entrypoint = options.installedMcpEntrypoint;
|
|
121
|
+
if (requested === "terminal" || !entrypoint || !isAbsolute(entrypoint))
|
|
122
|
+
throw new Error("Use --mcp-runtime installed with a packaged Relvo CLI and --client auto|claude|codex.");
|
|
123
|
+
const info = await lstat(entrypoint);
|
|
124
|
+
if (!info.isFile() || info.isSymbolicLink() || info.mode & 0o022 || typeof process.getuid === "function" && ![0, process.getuid()].includes(info.uid))
|
|
125
|
+
throw new Error("The installed MCP runtime has unsafe ownership or permissions.");
|
|
126
|
+
installedEntrypoint = await realpath(entrypoint);
|
|
127
|
+
}
|
|
128
|
+
const saved = values["api-url"] || values["console-url"] ? null : await readLocalProfile(path);
|
|
129
|
+
const local = values.local || saved?.local, requestedApi = values["api-url"] ?? saved?.apiUrl, requestedConsole = values["console-url"] ?? saved?.consoleUrl;
|
|
130
|
+
if (!requestedApi || !requestedConsole)
|
|
131
|
+
throw new Error("A first setup needs --api-url and --console-url. Copy the whole setup command from the Relvo console's onboarding page.");
|
|
132
|
+
const apiUrl = relvoApiUrl(requestedApi, local), consoleUrl = relvoApiUrl(requestedConsole, local);
|
|
133
|
+
if (new URL(consoleUrl).pathname !== "/")
|
|
134
|
+
throw new Error("--console-url must be the trusted Relvo console origin.");
|
|
135
|
+
if (local && [apiUrl, consoleUrl].some((url) => !["localhost", "127.0.0.1", "[::1]"].includes(new URL(url).hostname)))
|
|
136
|
+
throw new Error("--local requires loopback API and console URLs.");
|
|
137
|
+
const preparedVersion = options.version ?? PLATFORM_CLI_VERSION;
|
|
138
|
+
if (!/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/.test(preparedVersion))
|
|
139
|
+
throw new Error("The prepared Relvo package version is invalid.");
|
|
140
|
+
output("[1/4] Check the local environment and existing connection.\nRerun the same setup command after an interruption to resume safely.");
|
|
141
|
+
const agents = await preflightAgents(requested, env, home, runtime === "registry", output);
|
|
142
|
+
if (installedEntrypoint && !agents.length)
|
|
143
|
+
throw new Error("Use --mcp-runtime installed with a packaged Relvo CLI and --client auto|claude|codex.");
|
|
144
|
+
const selected = agents.length > 1 ? "agents" : agents[0]?.client ?? "terminal";
|
|
145
|
+
await withProfileLock(path, async () => {
|
|
146
|
+
let profile = await readLocalProfile(path);
|
|
147
|
+
if (profile && ((profile.apiUrl !== apiUrl && (profile.local || !onRelvoDomain(apiUrl))) || profile.consoleUrl !== consoleUrl || profile.local !== Boolean(local)))
|
|
148
|
+
throw new Error("A different Relvo login already owns this local profile. Log out explicitly before changing servers.");
|
|
149
|
+
if (!profile)
|
|
150
|
+
await removeLocalProjects(path);
|
|
151
|
+
const installation = profile ?? await readLocalInstallation(path);
|
|
152
|
+
const installationId = installation?.installationId ?? randomBytes(12).toString("hex");
|
|
153
|
+
const plans = [];
|
|
154
|
+
for (const { client, agent, launcher } of agents) {
|
|
155
|
+
const recorded = installation?.registrations.find((entry) => entry.client === client);
|
|
156
|
+
const installed = installedEntrypoint ? { client, name: `relvo-${installationId}`, command: await realpath(process.execPath), args: [installedEntrypoint, "--profile", path] } : null;
|
|
157
|
+
if (installed && recorded && (recorded.command !== installed.command || JSON.stringify(recorded.args) !== JSON.stringify(installed.args)))
|
|
158
|
+
throw new Error(`A different MCP runtime already owns the ${agentNames[client]} registration. It was preserved; use its original setup command.`);
|
|
159
|
+
const planned = recorded ?? installed ?? { client, name: `relvo-${installationId}`, command: launcher, args: ["--yes", "--ignore-scripts", "--registry=https://registry.npmjs.org", "--@relvo:registry=https://registry.npmjs.org", `--package=@relvo/cli@${preparedVersion}`, "relvo", "mcp", "--profile", path] };
|
|
160
|
+
if (await agentRegistration(client, agent, env, home, "relvo"))
|
|
161
|
+
throw new Error(`An existing fixed-name Relvo MCP entry in ${agentNames[client]} was preserved. Remove or rename it explicitly before automatic setup to avoid duplicate connections.`);
|
|
162
|
+
const existing = await agentRegistration(client, agent, env, home, planned.name);
|
|
163
|
+
if (existing && (!recorded || !registrationMatches(existing, recorded)))
|
|
164
|
+
throw new Error(`An unmanaged or changed relvo MCP server already exists in ${agentNames[client]}. Remove or rename it explicitly in your agent before setup.`);
|
|
165
|
+
plans.push({ agent, planned, existing });
|
|
166
|
+
}
|
|
167
|
+
if (profile && profile.apiUrl !== apiUrl) {
|
|
168
|
+
profile = { ...profile, apiUrl };
|
|
169
|
+
await writeLocalProfile(path, profile);
|
|
170
|
+
}
|
|
171
|
+
const setup = new LocalSetupClient(apiUrl, local, transport);
|
|
172
|
+
output("[2/4] Approve and verify the private login.");
|
|
173
|
+
if (profile?.state === "ready")
|
|
174
|
+
profile = await acknowledgeProfile(profile, setup, transport, now, path, deactivate);
|
|
175
|
+
const resumeProjectSetup = profile ? await projectSetupInProgress(path, profile) : false;
|
|
176
|
+
if (profile?.state === "pending" && profile.client !== selected)
|
|
177
|
+
throw new Error("The saved setup request belongs to another client. Resume that client or logout explicitly.");
|
|
178
|
+
const expired = profile && Date.parse(profile.state === "pending" ? profile.request.expiresAt : profile.grant.expiresAt) <= now();
|
|
179
|
+
if (expired)
|
|
180
|
+
output("The saved login or pairing request expired. A fresh browser approval is required; review the project and permissions before approving.");
|
|
181
|
+
if (profile?.state === "ready")
|
|
182
|
+
await rememberLocalProject(path, profile);
|
|
183
|
+
if (!profile || expired || values["new-project"] && profile.state === "ready" && !resumeProjectSetup) {
|
|
184
|
+
const request = await setup.start({ client: selected });
|
|
185
|
+
verificationUrl(request.verificationUri, consoleUrl, request.authorizationRequest, local);
|
|
186
|
+
if (Date.parse(request.expiresAt) <= now() || Date.parse(request.expiresAt) > now() + 601000)
|
|
187
|
+
throw new Error("Relvo returned an invalid setup expiry.");
|
|
188
|
+
profile = { managedBy: "relvo-cli", version: 1, installationId, state: "pending", apiUrl, consoleUrl, local: Boolean(local), client: selected, request, registrations: installation?.registrations ?? [] };
|
|
189
|
+
await writeLocalProfile(path, profile);
|
|
190
|
+
}
|
|
191
|
+
if (profile.state === "pending") {
|
|
192
|
+
const url = verificationUrl(profile.request.verificationUri, consoleUrl, profile.request.authorizationRequest, profile.local, values.project);
|
|
193
|
+
output(`Approve Relvo in your browser. Enter code ${profile.request.userCode} when asked.\n${url}\nDo not approve a request you did not start.`);
|
|
194
|
+
if (!values["no-browser"]) {
|
|
195
|
+
try {
|
|
196
|
+
await (options.openBrowser ?? ((value) => browser(value, env)))(url);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
output("Open the verification URL above manually to continue.");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const proof = { authorizationRequest: profile.request.authorizationRequest, deviceCode: profile.request.deviceCode };
|
|
203
|
+
const expires = Date.parse(profile.request.expiresAt);
|
|
204
|
+
for (;;) {
|
|
205
|
+
if (now() + 5000 >= expires)
|
|
206
|
+
throw new Error("This pairing window is ending. Rerun the same setup command; it resumes this request or renews it after expiry.");
|
|
207
|
+
await wait(5000);
|
|
208
|
+
const grant = await setup.poll(proof);
|
|
209
|
+
if (grant.status === "pending")
|
|
210
|
+
continue;
|
|
211
|
+
if (grant.status === "acknowledged")
|
|
212
|
+
throw new Error("Setup was acknowledged but its private key is missing locally. Revoke that key in the console, then run relvo logout and start setup again.");
|
|
213
|
+
if (grant.expiresAt !== profile.request.expiresAt || Date.parse(grant.expiresAt) <= now() || Date.parse(grant.approvedExpiresAt) <= expires)
|
|
214
|
+
throw new Error("Relvo returned an expired or inconsistent local approval. No new credentials were stored.");
|
|
215
|
+
if (values["new-project"])
|
|
216
|
+
await markProjectSetup(path, profile, grant.apiKeyId);
|
|
217
|
+
profile = { managedBy: "relvo-cli", version: 1, installationId, state: "ready", apiUrl, consoleUrl, local: Boolean(local), grant, pendingAck: proof, registrations: profile.registrations };
|
|
218
|
+
await writeLocalProfile(path, profile);
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
profile = await acknowledgeProfile(profile, setup, transport, now, path, deactivate);
|
|
223
|
+
const client = await inspectLogin(profile, transport, now);
|
|
224
|
+
await rememberLocalProject(path, profile, (await client.project()).data.name);
|
|
225
|
+
output(plans.length ? `[3/4] Install current guides and configure ${plans.map(({ planned }) => agentNames[planned.client]).join(" and ")}.` : "[3/4] Check the current guides for terminal use.");
|
|
226
|
+
const bundle = verifiedSkillBundle((await client.skills()).data);
|
|
227
|
+
const installFor = plans.map(({ planned }) => planned.client);
|
|
228
|
+
for (const { preserved } of await installSkills(home, installFor, bundle))
|
|
229
|
+
if (preserved.length)
|
|
230
|
+
output(`Kept your own guide: ${preserved.join(", ")}. Delete its folder and run relvo skills update to get the current one.`);
|
|
231
|
+
for (const { agent, planned, existing } of plans) {
|
|
232
|
+
if (existing)
|
|
233
|
+
continue;
|
|
234
|
+
profile.registrations = [...profile.registrations.filter((entry) => entry.client !== planned.client), planned];
|
|
235
|
+
await writeLocalProfile(path, profile);
|
|
236
|
+
await registerAgent(planned, agent, env, home);
|
|
237
|
+
}
|
|
238
|
+
output(plans.length ? "[4/4] Verify the bundled MCP runtime with read-only checks." : "[4/4] Verify CLI read-only access.");
|
|
239
|
+
for (const { planned } of plans)
|
|
240
|
+
await probeMcp(planned, env, home, profile.grant.developerProjectId);
|
|
241
|
+
if (!plans.length)
|
|
242
|
+
await inspectLogin(profile, transport, now);
|
|
243
|
+
const names = plans.map(({ planned }) => agentNames[planned.client]).join(" and ");
|
|
244
|
+
await completeProjectSetup(path, profile);
|
|
245
|
+
output(`Setup complete for project ${profile.grant.developerProjectId}. ${plans.length ? `${names} MCP ${plans.length > 1 ? "registrations" : "registration"} and current guides are prepared. Restart ${names} to load the connection and guides; host loading has not been verified.` : "CLI is ready."} No paid tools were run.`);
|
|
246
|
+
});
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (args[0] === "logout") {
|
|
250
|
+
if (args.length !== 1)
|
|
251
|
+
throw new Error("Usage: relvo logout");
|
|
252
|
+
await withProfileLock(path, async () => {
|
|
253
|
+
const profile = await readLocalProfile(path).catch(async (error) => {
|
|
254
|
+
if (!(error instanceof Error && error.name === "LocalProfileError" && "reason" in error && error.reason === "unreadable"))
|
|
255
|
+
throw error;
|
|
256
|
+
await removeUnreadableProfile(path);
|
|
257
|
+
output("Removed the unreadable login. Its key may still be active: revoke it in the Relvo console.");
|
|
258
|
+
return null;
|
|
259
|
+
});
|
|
260
|
+
await removeLocalProjects(path, profile?.installationId);
|
|
261
|
+
if (profile && await removeOwnedUpdateState(path, profile.installationId) === "preserved")
|
|
262
|
+
output("Foreign or unsafe Relvo update state was preserved; resolve it explicitly before configuring another installation.");
|
|
263
|
+
if (profile && await removeOwnedProjectSyncState(path, profile.installationId) === "preserved")
|
|
264
|
+
output("Foreign or unsafe Relvo project sync state was preserved; resolve it explicitly before configuring another installation.");
|
|
265
|
+
await removeLocalProfile(path);
|
|
266
|
+
});
|
|
267
|
+
output("Local Relvo login and saved project approvals removed. Revoke their keys in the Relvo console to disable server access. Installed guides and MCP registration remain.");
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (args[0] === "team-skills" && args[1] === "auto-sync") {
|
|
271
|
+
if (args.length !== 3 || !["on", "off", "status"].includes(args[2]))
|
|
272
|
+
throw new Error("Usage: relvo team-skills auto-sync on|off|status");
|
|
273
|
+
output(JSON.stringify(args[2] === "status" ? await projectSyncStatus(path, env, home) : await localProjectSync(path, env, { home }).configure(args[2] === "on", true), null, 2));
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (args[0] === "team-skills" && ["check", "apply", "dismiss"].includes(args[1])) {
|
|
277
|
+
const { values, positionals, tokens } = parseArgs({ args: args.slice(2), tokens: true, allowPositionals: true,
|
|
278
|
+
options: { client: { type: "string" }, yes: { type: "boolean" }, "allow-scripts": { type: "boolean" }, "remove-executable-settings": { type: "boolean" } } });
|
|
279
|
+
uniqueOptions(tokens);
|
|
280
|
+
const sync = localProjectSync(path, env, { home, selected: values.client, transport, now });
|
|
281
|
+
if (args[1] === "check" && !positionals.length)
|
|
282
|
+
output(JSON.stringify(await sync.check(), null, 2));
|
|
283
|
+
else if (args[1] !== "check" && positionals.length === 1)
|
|
284
|
+
output(JSON.stringify(args[1] === "apply" ? await sync.apply(positionals[0], values.yes === true, values["allow-scripts"] === true, values["remove-executable-settings"] === true)
|
|
285
|
+
: await sync.dismiss(positionals[0], values.yes === true), null, 2));
|
|
286
|
+
else
|
|
287
|
+
throw new Error("Usage: relvo team-skills check [--client <agents>] | apply|dismiss <snapshot> --yes [--client <agents>] [--allow-scripts | --remove-executable-settings]");
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (args[0] === "team-skills" && args[1] === "sync") {
|
|
291
|
+
const { values, tokens } = parseArgs({ args: args.slice(2), tokens: true, options: { client: { type: "string" }, project: { type: "string" }, "allow-scripts": { type: "boolean" } } });
|
|
292
|
+
uniqueOptions(tokens);
|
|
293
|
+
if (values.project !== undefined && !values.project.trim())
|
|
294
|
+
throw new Error("Provide a project ID after --project.");
|
|
295
|
+
output(JSON.stringify(await syncProjectFiles(path, env, { home, selected: values.client, projectId: values.project, allowScripts: values["allow-scripts"], transport, now }), null, 2));
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const saved = await readLocalProfile(path);
|
|
299
|
+
if (!saved || saved.state !== "ready")
|
|
300
|
+
throw new Error("Run relvo setup to create or resume the local login.");
|
|
301
|
+
await runtimeConfig(env, "cli", path, true);
|
|
302
|
+
const profile = saved;
|
|
303
|
+
const client = await inspectLogin(profile, transport, now);
|
|
304
|
+
if (args[0] === "doctor") {
|
|
305
|
+
if (args.length !== 1)
|
|
306
|
+
throw new Error("Usage: relvo doctor");
|
|
307
|
+
await listLocalProjects(path, now());
|
|
308
|
+
for (const registration of profile.registrations) {
|
|
309
|
+
const { agent } = await preflightAgent(registration.client, env, home, false, "doctor");
|
|
310
|
+
if (!agent || !registrationMatches(await agentRegistration(registration.client, agent, env, home, registration.name), registration))
|
|
311
|
+
throw new Error("An owned Relvo MCP registration is missing or changed. Rerun setup after resolving the conflict.");
|
|
312
|
+
await probeMcp(registration, env, home, profile.grant.developerProjectId);
|
|
313
|
+
}
|
|
314
|
+
const bundle = verifiedSkillBundle((await client.skills()).data);
|
|
315
|
+
output(`Relvo login and tool access verified. Owned MCP registrations and bundled-runtime probes verified: ${profile.registrations.length}. Current distributed guide bundle: ${bundle.version}. Host loading and installed local guides were not checked. No paid tools were run.`);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (args[0] === "skills" && args[1] === "list" && args.length === 2) {
|
|
319
|
+
const bundle = verifiedSkillBundle((await client.skills()).data);
|
|
320
|
+
output(JSON.stringify({ version: bundle.version, skills: bundle.skills.map(({ name, description, sha256 }) => ({ name, description, sha256 })) }, null, 2));
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (args[0] === "skills" && args[1] === "update") {
|
|
324
|
+
const { values, tokens } = parseArgs({ args: args.slice(2), tokens: true, options: { client: { type: "string" } } });
|
|
325
|
+
uniqueOptions(tokens);
|
|
326
|
+
const selected = choice("client", ["claude", "codex", "all"], values.client ?? "all");
|
|
327
|
+
const clients = selected === "all" ? profile.registrations.length ? profile.registrations.map((entry) => entry.client) : ["claude", "codex"] : [selected];
|
|
328
|
+
await withProfileLock(path, async () => { output(JSON.stringify(await installSkills(home, clients, (await client.skills()).data), null, 2)); });
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (args[0] === "team-skills" && args[1] === "push") {
|
|
332
|
+
const { values, positionals, tokens } = parseArgs({ args: args.slice(2), tokens: true, allowPositionals: true, options: { slug: { type: "string" }, kind: { type: "string" } } });
|
|
333
|
+
uniqueOptions(tokens);
|
|
334
|
+
if (positionals.length !== 1)
|
|
335
|
+
throw new Error("Usage: relvo team-skills push <SKILL.md, folder, AGENTS.md, CLAUDE.md, agent or rule file> [--kind skill|instructions|agent|rule] [--slug <slug>]");
|
|
336
|
+
const kind = values.kind === undefined ? harnessKindOf(positionals[0]) ?? "skill" : choice("kind", ["skill", "instructions", "agent", "rule"], values.kind);
|
|
337
|
+
await withProfileLock(path, async () => {
|
|
338
|
+
const roots = harnessRoots(home, env, await readCustomHarnesses(home));
|
|
339
|
+
output(JSON.stringify(kind === "skill" ? await pushTeamSkill(home, profile.grant.developerProjectId, client, positionals[0], values.slug, roots)
|
|
340
|
+
: await pushTeamHarnessFile(roots, profile.grant.developerProjectId, client, positionals[0], kind, values.slug), null, 2));
|
|
341
|
+
});
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (args[0] === "team-skills")
|
|
345
|
+
throw new Error(`Use relvo team-skills check, apply <snapshot> --yes, auto-sync on|off|status, sync [--client all|<${TEAM_HARNESSES.join("|")}>[,…]] [--project <id>] [--allow-scripts], or push <SKILL.md, folder, agent or rule file>.`);
|
|
346
|
+
throw new Error("Use relvo skills list or relvo skills update [--client claude|codex|all].");
|
|
347
|
+
}
|