robisie 0.0.0-stage → 0.1.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/CHANGELOG.md +18 -0
- package/README.md +27 -2
- package/dist/agents.js +185 -0
- package/dist/cli.js +137 -0
- package/dist/commands.js +318 -0
- package/dist/constants.js +34 -0
- package/dist/context.js +65 -0
- package/dist/fsops.js +438 -0
- package/dist/helpers.js +95 -0
- package/dist/manifest.js +156 -0
- package/dist/mcp.js +127 -0
- package/dist/roles.js +119 -0
- package/docs/README.en.md +174 -0
- package/docs/README.md +174 -0
- package/docs/RELEASE-0.1.0.md +29 -0
- package/helpers/ceo-client-launcher-mcp.mjs +579 -0
- package/helpers/client-launch-context-mcp.mjs +538 -0
- package/helpers/lib/client-review-receipt.mjs +705 -0
- package/helpers/lib/client-spawn-failure.mjs +129 -0
- package/package.json +30 -4
- package/roles/ceo-client.md +192 -0
- package/roles/manager-builder-client.md +106 -0
- package/roles/quality-officer-client.md +191 -0
package/dist/commands.js
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { readdirSync, rmdirSync, rmSync } from "node:fs";
|
|
2
|
+
import { AGENT_LABELS, SHELL_EXPORT, agentPaths, agentOnPath, detectAgents, planAgent, removeCodexTables, shellProfile } from "./agents.js";
|
|
3
|
+
import { HELPER_SERVERS, KEY_ENV, KEY_PATTERN, PACKAGE_NAME, PACKAGE_VERSION } from "./constants.js";
|
|
4
|
+
import { RobisieError, errorMessage } from "./context.js";
|
|
5
|
+
import { applyOp, assertCodexChange, capture, entryPresent, prepareBlock, refusePrivateSymlink, restore, undoEntry } from "./fsops.js";
|
|
6
|
+
import { HELPER_TOOLS, probeHelper } from "./helpers.js";
|
|
7
|
+
import { clientPaths, ensurePrivateDir, readKey, readManifest, writeKey, writeManifest, roleProviders, ROLE_IDS, ROLE_STEP } from "./manifest.js";
|
|
8
|
+
import { assertCompatible, checkConnection, fetchContractVersion } from "./mcp.js";
|
|
9
|
+
import { loadRoles, rolesDir } from "./roles.js";
|
|
10
|
+
const NO_KEY_STEP = "Wygeneruj klucz worker na https://robisie.app/dashboard/connect; w zsh/bash wykonaj `unset ROBISIE_KEY`, potem lokalne `npx robisie init` i wklej klucz w ukrytym pytaniu (poza rozmową).";
|
|
11
|
+
function entryId(entry) {
|
|
12
|
+
return `${entry.kind}:${entry.path}:${entry.keyPath?.join(".") ?? ""}`;
|
|
13
|
+
}
|
|
14
|
+
async function obtainKey(ctx, keyStdin) {
|
|
15
|
+
let key = ctx.env[KEY_ENV]?.trim() ?? "";
|
|
16
|
+
if (!key && ctx.askSecret)
|
|
17
|
+
key = (await ctx.askSecret("Wklej klucz robisie (rob_…, nie będzie widoczny): ")).trim();
|
|
18
|
+
if (!key && keyStdin && ctx.readStdin)
|
|
19
|
+
key = (await ctx.readStdin()).trim().split(/\r?\n/)[0]?.trim() ?? "";
|
|
20
|
+
if (!key)
|
|
21
|
+
throw new RobisieError("Brak klucza robisie.", NO_KEY_STEP);
|
|
22
|
+
if (!KEY_PATTERN.test(key))
|
|
23
|
+
throw new RobisieError("To nie wygląda na klucz robisie (oczekuję rob_ i 64 znaków 0-9a-f).", "Skopiuj klucz jeszcze raz z https://robisie.app/dashboard/connect i uruchom `npx robisie init`.");
|
|
24
|
+
return key;
|
|
25
|
+
}
|
|
26
|
+
export async function init(ctx, options) {
|
|
27
|
+
const paths = clientPaths(agentPaths(ctx).clientDir);
|
|
28
|
+
const prior = readManifest(paths.manifest);
|
|
29
|
+
const detected = options.agents ?? detectAgents(ctx);
|
|
30
|
+
const previousMap = prior ? roleProviders(prior) : roleProviders({ agents: detected });
|
|
31
|
+
let map = previousMap;
|
|
32
|
+
if (options.roles && Object.keys(options.roles).length > 0) {
|
|
33
|
+
const merged = { ...previousMap, ...options.roles };
|
|
34
|
+
if (!ROLE_IDS.every((role) => merged[role]))
|
|
35
|
+
throw new RobisieError("Wynikowa mapa ról jest niekompletna (ceo, builder, quality).", ROLE_STEP);
|
|
36
|
+
map = roleProviders({ agents: [], roleProviders: merged });
|
|
37
|
+
}
|
|
38
|
+
const selected = [...new Set([...Object.values(map ?? {}), ...detected])];
|
|
39
|
+
if (selected.length === 0) {
|
|
40
|
+
throw new RobisieError("Nie znalazłem na tym komputerze Claude Code, Codex CLI ani OpenCode.", "Zainstaluj jednego z tych agentów albo wskaż go wprost, np. `npx robisie init --agent codex`.");
|
|
41
|
+
}
|
|
42
|
+
const key = await obtainKey(ctx, options.keyStdin);
|
|
43
|
+
// Refuse before writing anything: incompatible server, or a key that does not open the board.
|
|
44
|
+
assertCompatible(await fetchContractVersion(ctx));
|
|
45
|
+
const connection = await checkConnection(ctx, key);
|
|
46
|
+
if (connection.contractVersion)
|
|
47
|
+
assertCompatible(connection.contractVersion);
|
|
48
|
+
const roles = loadRoles(options.rolesDir ?? rolesDir());
|
|
49
|
+
const planned = [];
|
|
50
|
+
for (const id of selected)
|
|
51
|
+
for (const op of planAgent(ctx, id, roles, key))
|
|
52
|
+
planned.push({ owner: id, op });
|
|
53
|
+
const profile = options.shell ? shellProfile(ctx) : null;
|
|
54
|
+
if (profile)
|
|
55
|
+
planned.push({ owner: "shell", op: { kind: "block", path: profile, comment: "hash", body: SHELL_EXPORT } });
|
|
56
|
+
// Before writing anything, so a refusal leaves no half-done install behind.
|
|
57
|
+
for (const { op } of planned) {
|
|
58
|
+
refusePrivateSymlink(op);
|
|
59
|
+
if (op.kind === "block" && op.clean)
|
|
60
|
+
prepareBlock(capture(op.path).bytes?.toString("utf8") ?? null, op.comment, op.clean, op.body);
|
|
61
|
+
}
|
|
62
|
+
ensurePrivateDir(paths.dir);
|
|
63
|
+
const priorById = new Map((prior?.entries ?? []).map((entry) => [entryId(entry), entry]));
|
|
64
|
+
const done = new Map(priorById);
|
|
65
|
+
const agents = [...new Set([...(prior?.agents ?? []), ...selected])];
|
|
66
|
+
let changed = 0;
|
|
67
|
+
// The record first, then the key and the changes: a disk that fills up cannot leave a key or a change uninstall does not know about.
|
|
68
|
+
writeManifest(paths.dir, { agents, roleProviders: map, entries: [...done.values()] });
|
|
69
|
+
writeKey(paths.dir, key);
|
|
70
|
+
for (const { owner, op } of planned) {
|
|
71
|
+
const id = entryId(op);
|
|
72
|
+
const before = capture(op.path);
|
|
73
|
+
const applied = applyOp(op, priorById.get(id), { backupDir: paths.backups });
|
|
74
|
+
const previous = done.get(id);
|
|
75
|
+
done.set(id, { ...applied.entry, owner });
|
|
76
|
+
try {
|
|
77
|
+
// Recorded after every change (atomically), so even a killed init leaves uninstall a complete record.
|
|
78
|
+
writeManifest(paths.dir, { agents, roleProviders: map, entries: [...done.values()] });
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
// The change is not on record, so uninstall could never undo it: take it back before failing.
|
|
82
|
+
if (previous)
|
|
83
|
+
done.set(id, previous);
|
|
84
|
+
else
|
|
85
|
+
done.delete(id);
|
|
86
|
+
try {
|
|
87
|
+
if (op.kind === "block" && op.clean) {
|
|
88
|
+
const original = before.bytes?.toString("utf8") ?? "";
|
|
89
|
+
assertCodexChange(capture(op.path).bytes?.toString("utf8") ?? "", original, original);
|
|
90
|
+
}
|
|
91
|
+
restore(before);
|
|
92
|
+
}
|
|
93
|
+
catch (rollback) {
|
|
94
|
+
throw new RobisieError(`Zmieniłem ${op.path}, ale nie udało się zapisać rekordu zmian ani cofnąć tej zmiany: ${errorMessage(error)}; ${errorMessage(rollback)}`, `Zwolnij miejsce na dysku i przywróć ${op.path}${applied.entry.backup ? ` z kopii ${applied.entry.backup}` : " (usuń z niego wpis robisie)"}.`);
|
|
95
|
+
}
|
|
96
|
+
throw new RobisieError(errorMessage(error), `${error instanceof RobisieError ? error.nextStep : "Ponów init."} Kopie zapasowe: ${paths.backups}.`);
|
|
97
|
+
}
|
|
98
|
+
if (applied.changed)
|
|
99
|
+
changed += 1;
|
|
100
|
+
}
|
|
101
|
+
ctx.out(`robisie ${PACKAGE_VERSION}: połączono z tablicą (projektów: ${connection.projectCount}).`);
|
|
102
|
+
for (const id of selected)
|
|
103
|
+
ctx.out(`✓ ${AGENT_LABELS[id]}: serwer MCP robisie i role klienta (CEO, Builder, Quality Officer) zainstalowane.`);
|
|
104
|
+
ctx.out(changed === 0 ? "Nic się nie zmieniło — wszystko było już zainstalowane." : `Zmienione pliki: ${changed} (kopie zapasowe: ${paths.backups}).`);
|
|
105
|
+
if (profile)
|
|
106
|
+
ctx.out(`✓ Klucz będzie dostępny jako ${KEY_ENV} w nowych terminalach (wpis w ${profile}).`);
|
|
107
|
+
else
|
|
108
|
+
ctx.out(`! Ustaw sam zmienną ${KEY_ENV} dla agenta, np.: ${SHELL_EXPORT}`);
|
|
109
|
+
ctx.out("Następny krok: otwórz nowy terminal i uruchom `npx robisie doctor`.");
|
|
110
|
+
}
|
|
111
|
+
// ── doctor ───────────────────────────────────────────────────────────────────
|
|
112
|
+
/** Returns the process exit code: 0 = all good, 1 = something needs one action. */
|
|
113
|
+
export async function doctor(ctx) {
|
|
114
|
+
const problems = [];
|
|
115
|
+
const fail = (line, nextStep) => {
|
|
116
|
+
ctx.out(`✗ ${line}`);
|
|
117
|
+
problems.push(nextStep);
|
|
118
|
+
};
|
|
119
|
+
const guard = async (action) => {
|
|
120
|
+
try {
|
|
121
|
+
await action();
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (error instanceof RobisieError)
|
|
125
|
+
fail(error.message, error.nextStep);
|
|
126
|
+
else
|
|
127
|
+
fail(`Nieoczekiwany błąd: ${errorMessage(error)}`, "Uruchom lokalne `npx robisie doctor` w katalogu instalacji; jeśli błąd zostaje, przekaż zespołowi komunikat i wersję paczki bez klucza.");
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
ctx.out(`Paczka ${PACKAGE_NAME} ${PACKAGE_VERSION}`);
|
|
131
|
+
const paths = clientPaths(agentPaths(ctx).clientDir);
|
|
132
|
+
await guard(async () => {
|
|
133
|
+
const version = await fetchContractVersion(ctx);
|
|
134
|
+
assertCompatible(version);
|
|
135
|
+
ctx.out(`✓ Wersja kontraktu serwera ${version} pasuje do paczki.`);
|
|
136
|
+
});
|
|
137
|
+
const envKey = ctx.env[KEY_ENV]?.trim() || null;
|
|
138
|
+
let key = envKey;
|
|
139
|
+
await guard(async () => {
|
|
140
|
+
key = envKey ?? readKey(paths.dir);
|
|
141
|
+
});
|
|
142
|
+
const usableKey = key;
|
|
143
|
+
if (!usableKey)
|
|
144
|
+
fail("Brak klucza robisie.", NO_KEY_STEP);
|
|
145
|
+
else if (!KEY_PATTERN.test(usableKey))
|
|
146
|
+
fail("Zapisany klucz ma zły format.", "Uruchom `npx robisie init` i wklej klucz jeszcze raz.");
|
|
147
|
+
else {
|
|
148
|
+
await guard(async () => {
|
|
149
|
+
const connection = await checkConnection(ctx, usableKey);
|
|
150
|
+
if (connection.contractVersion)
|
|
151
|
+
assertCompatible(connection.contractVersion);
|
|
152
|
+
ctx.out(`✓ Połączenie działa: klucz otwiera tablicę (projektów: ${connection.projectCount}).`);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
let manifest = null;
|
|
156
|
+
await guard(async () => {
|
|
157
|
+
manifest = readManifest(paths.manifest);
|
|
158
|
+
});
|
|
159
|
+
const installed = manifest;
|
|
160
|
+
let map = null;
|
|
161
|
+
if (installed)
|
|
162
|
+
await guard(async () => { map = roleProviders(installed); });
|
|
163
|
+
const assignment = map;
|
|
164
|
+
for (const role of ROLE_IDS)
|
|
165
|
+
ctx.out(`${role} → ${assignment?.[role] ?? "nie przypisano"}`);
|
|
166
|
+
if (!installed || installed.agents.length === 0) {
|
|
167
|
+
fail("robisie nie jest zainstalowane dla żadnego agenta.", "Uruchom `npx robisie init`.");
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const byId = new Map(installed.entries.map((entry) => [entryId(entry), entry]));
|
|
171
|
+
for (const id of new Set([...installed.agents, ...Object.values(assignment ?? {})])) {
|
|
172
|
+
const repair = `Uruchom \`npx robisie init --agent ${id}\` — uzupełni braki.`;
|
|
173
|
+
await guard(async () => {
|
|
174
|
+
// A complete install = every change init plans for this agent is recorded AND still in place.
|
|
175
|
+
const expected = planAgent(ctx, id, loadRoles(), usableKey ?? `rob_${"0".repeat(64)}`);
|
|
176
|
+
const missing = expected.filter((op) => {
|
|
177
|
+
const entry = byId.get(entryId(op));
|
|
178
|
+
if (!entry || entry.owner !== id || !entryPresent(entry))
|
|
179
|
+
return true;
|
|
180
|
+
if (op.kind === "block" && op.clean) {
|
|
181
|
+
try {
|
|
182
|
+
const current = capture(op.path).bytes?.toString("utf8") ?? "";
|
|
183
|
+
assertCodexChange(current, current, op.body);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// The marker alone is insufficient: report missing/invalid MCP tables as incomplete installation.
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return false;
|
|
191
|
+
}).map(entryId);
|
|
192
|
+
const roleIds = expected.filter((op) => op.kind === "file").map(entryId);
|
|
193
|
+
const rolesOk = roleIds.filter((eid) => !missing.includes(eid)).length;
|
|
194
|
+
const configOk = missing.every((eid) => eid.startsWith("file:"));
|
|
195
|
+
if (missing.length === 0)
|
|
196
|
+
ctx.out(`✓ ${AGENT_LABELS[id]}: konfiguracja MCP, pomocniki i role (${rolesOk}/${roleIds.length}) na miejscu.`);
|
|
197
|
+
else {
|
|
198
|
+
const assigned = assignment ? ROLE_IDS.filter((role) => assignment[role] === id) : [];
|
|
199
|
+
const detail = assigned.map((role) => `${role} → ${id}: niepełna instalacja`).join(", ");
|
|
200
|
+
fail(`${AGENT_LABELS[id]}: niepełna instalacja — konfiguracja MCP ${configOk ? "jest" : "niekompletna"}, role ${rolesOk}/${roleIds.length} (brak m.in. ${missing[0]?.split(":")[1] ?? "?"}).${detail ? ` Role: ${detail}.` : ""}`, repair);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
for (const id of new Set(assignment ? Object.values(assignment) : installed.agents)) {
|
|
205
|
+
if (!agentOnPath(ctx, id)) {
|
|
206
|
+
const assigned = assignment ? ROLE_IDS.filter((role) => assignment[role] === id).join(", ") : "nie przypisano";
|
|
207
|
+
fail(`${assigned} → ${id}: ${detectAgents(ctx).includes(id) ? "katalog jest, programu nie ma w PATH" : "brak programu w PATH i katalogu dostawcy"}.`, `Zainstaluj program ${id} lub dodaj go do PATH i ponów \`npx robisie doctor\`.`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (!assignment)
|
|
211
|
+
fail("Role: nie przypisano dostawców.", `Uruchom npx robisie init z pełną mapą. ${ROLE_STEP}`);
|
|
212
|
+
else if (assignment.builder !== assignment.ceo || assignment.quality !== assignment.ceo) {
|
|
213
|
+
fail(`Mieszana mapa: Quality Officer jako podagent u dostawcy CEO (${assignment.ceo}); polecenie Buildera w formacie dostawcy CEO (${assignment.ceo}). Wykonanie mieszanych ról to następny zakres.`, "Wybierz jednego dostawcę dla trzech ról przez `npx robisie init --role ceo=claude --role builder=claude --role quality=claude` albo zachowaj mapę i poczekaj na obsługę mieszanych ról.");
|
|
214
|
+
}
|
|
215
|
+
for (const name of HELPER_SERVERS) {
|
|
216
|
+
await guard(async () => {
|
|
217
|
+
let tools;
|
|
218
|
+
try {
|
|
219
|
+
tools = await probeHelper(name, ctx.env, ctx.cwd);
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
throw new RobisieError(`Pomocnik ${name} nie działa na tym komputerze: ${errorMessage(error)}`, "Sprawdź Node.js (wymagany 20+). Przeinstaluj otrzymany plik .tgz w katalogu instalacji: `npm i ./robisie-0.1.0.tgz` (użyj nazwy otrzymanego pliku), potem lokalne `npx robisie init` i `npx robisie doctor`.");
|
|
223
|
+
}
|
|
224
|
+
const absent = HELPER_TOOLS[name].filter((tool) => !tools.includes(tool));
|
|
225
|
+
if (absent.length > 0)
|
|
226
|
+
throw new RobisieError(`Pomocnik ${name} nie ma narzędzi: ${absent.join(", ")}.`, "Przeinstaluj otrzymany plik .tgz w katalogu instalacji: `npm i ./robisie-0.1.0.tgz` (użyj nazwy otrzymanego pliku), potem lokalne `npx robisie init` i `npx robisie doctor`.");
|
|
227
|
+
ctx.out(`✓ Pomocnik ${name} działa (${tools.length} narzędzi).`);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (!envKey)
|
|
232
|
+
fail(`Zmienna ${KEY_ENV} nie jest ustawiona — role i agenci jej potrzebują.`, `Jeśli init zapisał profil zsh/bash, otwórz nowy terminal z tym samym XDG_CONFIG_HOME. Przy --no-shell/innej powłoce ustaw środowisko agenta bez ujawniania klucza; w powłoce POSIX: ${SHELL_EXPORT}`);
|
|
233
|
+
if (problems.length === 0) {
|
|
234
|
+
ctx.out("Wszystko działa. Uruchom swojego agenta i poproś o rolę robisie CEO.");
|
|
235
|
+
return 0;
|
|
236
|
+
}
|
|
237
|
+
ctx.out(`Następny krok: ${problems[0]}`);
|
|
238
|
+
return 1;
|
|
239
|
+
}
|
|
240
|
+
// ── uninstall ────────────────────────────────────────────────────────────────
|
|
241
|
+
/** Remove a directory only when it is empty; anything left inside is not ours to delete. */
|
|
242
|
+
function removeEmptyDir(path) {
|
|
243
|
+
try {
|
|
244
|
+
rmdirSync(path);
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
const code = error.code;
|
|
249
|
+
if (code === "ENOENT")
|
|
250
|
+
return true;
|
|
251
|
+
if (code === "ENOTEMPTY" || code === "EEXIST")
|
|
252
|
+
return false;
|
|
253
|
+
throw new RobisieError(`Nie mogę usunąć katalogu ${path}: ${errorMessage(error)}`, `Usuń katalog ${path} ręcznie.`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
export function uninstall(ctx) {
|
|
257
|
+
const agent = agentPaths(ctx);
|
|
258
|
+
const paths = clientPaths(agent.clientDir);
|
|
259
|
+
const manifest = readManifest(paths.manifest);
|
|
260
|
+
let hasBackups = false;
|
|
261
|
+
try {
|
|
262
|
+
hasBackups = readdirSync(paths.backups).length > 0;
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
if (error.code !== "ENOENT") {
|
|
266
|
+
throw new RobisieError(`Nie mogę sprawdzić kopii zapasowych: ${errorMessage(error)}`, `Sprawdź uprawnienia do ${paths.backups} i ponów polecenie.`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const backupNotice = hasBackups ? `Kopie zapasowe pozostają w ${paths.backups}.` : "";
|
|
270
|
+
if (!manifest) {
|
|
271
|
+
ctx.out("Nie znaleziono zapisu instalacji — nic nie zmieniono (0 zmian).");
|
|
272
|
+
ctx.out(`Jeśli wpis robisie pozostał, usuń go ręcznie z konfiguracji swojego agenta: ${agent.claudeJson}, ${agent.codexConfig} lub ${agent.opencodeJson} (albo ${agent.opencodeJsonc}).`);
|
|
273
|
+
if (backupNotice)
|
|
274
|
+
ctx.out(backupNotice);
|
|
275
|
+
return 0;
|
|
276
|
+
}
|
|
277
|
+
const pending = new Set(manifest.entries);
|
|
278
|
+
// The manifest records the installed location; CODEX_HOME may have changed since init.
|
|
279
|
+
const codexConfigs = new Set(manifest.entries.filter((entry) => entry.kind === "block" && entry.comment === "hash" && (entry.owner === "codex" || entry.path === agent.codexConfig)).map((entry) => entry.path));
|
|
280
|
+
const evidence = `${backupNotice ? `${backupNotice} ` : ""}Pozostałe zmiany: ${paths.manifest}.`;
|
|
281
|
+
try {
|
|
282
|
+
// Refuse unsafe Codex edits before deleting the key, undoing roles or recording progress.
|
|
283
|
+
for (const entry of manifest.entries) {
|
|
284
|
+
if (entry.kind === "block" && codexConfigs.has(entry.path)) {
|
|
285
|
+
prepareBlock(capture(entry.path).bytes?.toString("utf8") ?? null, entry.comment, (text) => removeCodexTables(text, true));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// The key is not needed to finish an uninstall, even after a warning or a failure.
|
|
289
|
+
rmSync(paths.key, { force: true });
|
|
290
|
+
for (const entry of [...manifest.entries].reverse()) {
|
|
291
|
+
const warning = undoEntry(entry, codexConfigs.has(entry.path) ? (text) => removeCodexTables(text, true) : undefined);
|
|
292
|
+
if (warning)
|
|
293
|
+
ctx.out(`! ${warning} ${evidence}`);
|
|
294
|
+
else {
|
|
295
|
+
pending.delete(entry);
|
|
296
|
+
// A later init must not mistake an already restored user role for an edited installed one.
|
|
297
|
+
writeManifest(paths.dir, { agents: manifest.agents, roleProviders: manifest.roleProviders, entries: [...pending] });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (pending.size > 0) {
|
|
301
|
+
ctx.out(`! Nie udało się cofnąć wszystkich zmian. ${evidence}`);
|
|
302
|
+
ctx.out("Następny krok: porównaj pozostawione pliki z kopiami, zachowaj swoje zmiany i ponów `npx robisie uninstall`.");
|
|
303
|
+
return 1;
|
|
304
|
+
}
|
|
305
|
+
rmSync(paths.manifest, { force: true });
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
throw new RobisieError(`Nie udało się dokończyć odinstalowania: ${errorMessage(error)}`, `${error instanceof RobisieError ? error.nextStep : "Rozwiąż problem i ponów `npx robisie uninstall`."} ${evidence}`);
|
|
309
|
+
}
|
|
310
|
+
const leftovers = [paths.codexRoles, paths.roles, paths.dir].filter((dir) => !removeEmptyDir(dir));
|
|
311
|
+
ctx.out(`✓ Usunięto wpisy i bloki robisie oraz cofnięto instalację ról (${manifest.entries.length} zmian).`);
|
|
312
|
+
if (backupNotice)
|
|
313
|
+
ctx.out(backupNotice);
|
|
314
|
+
if (leftovers.length > 0)
|
|
315
|
+
ctx.out(`Zostawiłem niepuste katalogi: ${leftovers.join(", ")}.`);
|
|
316
|
+
ctx.out("Następny krok: otwórz nowy terminal, żeby zmienna ROBISIE_KEY zniknęła z sesji.");
|
|
317
|
+
return 0;
|
|
318
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Package identity and the server contract it speaks. */
|
|
2
|
+
export const PACKAGE_NAME = "robisie";
|
|
3
|
+
export const PACKAGE_VERSION = "0.1.0"; // x-release-please-version
|
|
4
|
+
/**
|
|
5
|
+
* MAJOR of the agent-API contract this package understands.
|
|
6
|
+
* PARITY: src/lib/mcp/contract-version.ts (AGENT_API_CONTRACT_VERSION) on the server.
|
|
7
|
+
*/
|
|
8
|
+
export const SUPPORTED_CONTRACT_MAJOR = 1;
|
|
9
|
+
export const DEFAULT_ORIGIN = "https://robisie.app";
|
|
10
|
+
export const MCP_SERVER_NAME = "robisie";
|
|
11
|
+
export const KEY_ENV = "ROBISIE_KEY";
|
|
12
|
+
/**
|
|
13
|
+
* Local stdio helpers the client roles call (`mcp__client-launch-context__…`, `mcp__ceo-client-launcher__…`).
|
|
14
|
+
* Shipped in `helpers/` (verbatim copies of the repo's scripts/) and started as `npx -y robisie@<version> mcp <name>`.
|
|
15
|
+
*/
|
|
16
|
+
export const HELPER_SERVERS = ["client-launch-context", "ceo-client-launcher"];
|
|
17
|
+
/** Environment the helpers read: credential, the Builder's card and the selected project directory. */
|
|
18
|
+
export const HELPER_ENV = [KEY_ENV, "ROBISIE_CARD_REF", "ROBISIE_PROJECT_CONTEXT"];
|
|
19
|
+
export function helperCommand(name) {
|
|
20
|
+
return { command: "npx", args: ["-y", `${PACKAGE_NAME}@${PACKAGE_VERSION}`, "mcp", name] };
|
|
21
|
+
}
|
|
22
|
+
export const KEY_PATTERN = /^rob_[0-9a-f]{64}$/;
|
|
23
|
+
export const CONTRACT_VERSION_HEADER = "x-robisie-contract-version";
|
|
24
|
+
/** Text markers delimiting what `init` wrote. `uninstall` removes only what sits between them. */
|
|
25
|
+
export const MARK_BEGIN = ">>> robisie (zarządzane przez `npx robisie`, nie edytuj) >>>";
|
|
26
|
+
export const MARK_END = "<<< robisie <<<";
|
|
27
|
+
/** First-line marker of every role file `init` owns. */
|
|
28
|
+
export const FILE_MARK = "robisie:managed";
|
|
29
|
+
export function mcpUrl(origin) {
|
|
30
|
+
return `${origin}/api/mcp`;
|
|
31
|
+
}
|
|
32
|
+
export function wellKnownUrl(origin) {
|
|
33
|
+
return `${origin}/.well-known/mcp.json`;
|
|
34
|
+
}
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import { Writable } from "node:stream";
|
|
4
|
+
import { DEFAULT_ORIGIN } from "./constants.js";
|
|
5
|
+
/** A failure the client can act on: what happened + exactly one next step (both in Polish). */
|
|
6
|
+
export class RobisieError extends Error {
|
|
7
|
+
nextStep;
|
|
8
|
+
constructor(message, nextStep) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.nextStep = nextStep;
|
|
11
|
+
this.name = "RobisieError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function errorMessage(error) {
|
|
15
|
+
return error instanceof Error ? error.message : String(error);
|
|
16
|
+
}
|
|
17
|
+
/** One POSIX shell word: single-quoted, each `'` written as `'\''` (paths like `/Users/O'Connor`). */
|
|
18
|
+
export function shellQuote(value) {
|
|
19
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
20
|
+
}
|
|
21
|
+
function hiddenPrompt(question) {
|
|
22
|
+
let muted = false;
|
|
23
|
+
const output = new Writable({
|
|
24
|
+
write(chunk, _encoding, callback) {
|
|
25
|
+
if (!muted)
|
|
26
|
+
process.stdout.write(chunk);
|
|
27
|
+
callback();
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
const rl = createInterface({ input: process.stdin, output, terminal: true });
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
rl.on("SIGINT", () => {
|
|
33
|
+
rl.close();
|
|
34
|
+
reject(new RobisieError("Przerwano.", "Uruchom ponownie `npx robisie init`."));
|
|
35
|
+
});
|
|
36
|
+
rl.question(question, (answer) => {
|
|
37
|
+
rl.close();
|
|
38
|
+
process.stdout.write("\n");
|
|
39
|
+
resolve(answer);
|
|
40
|
+
});
|
|
41
|
+
muted = true;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async function readAllStdin() {
|
|
45
|
+
const chunks = [];
|
|
46
|
+
for await (const chunk of process.stdin) {
|
|
47
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
48
|
+
}
|
|
49
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
50
|
+
}
|
|
51
|
+
export function nodeCtx() {
|
|
52
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
53
|
+
return {
|
|
54
|
+
home: homedir(),
|
|
55
|
+
cwd: process.cwd(),
|
|
56
|
+
env: process.env,
|
|
57
|
+
platform: process.platform,
|
|
58
|
+
fetch: globalThis.fetch,
|
|
59
|
+
out: (line) => process.stdout.write(`${line}\n`),
|
|
60
|
+
err: (line) => process.stderr.write(`${line}\n`),
|
|
61
|
+
askSecret: interactive ? hiddenPrompt : null,
|
|
62
|
+
readStdin: process.stdin.isTTY ? null : readAllStdin,
|
|
63
|
+
origin: DEFAULT_ORIGIN,
|
|
64
|
+
};
|
|
65
|
+
}
|