robisie 0.1.0 → 0.1.1
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 +69 -0
- package/README.md +76 -12
- package/dist/active-recovery.d.ts +58 -0
- package/dist/active-recovery.js +143 -0
- package/dist/agents.d.ts +34 -0
- package/dist/agents.js +77 -16
- package/dist/attempt-identity.d.ts +21 -0
- package/dist/attempt-identity.js +39 -0
- package/dist/ci-failure-result.d.ts +23 -0
- package/dist/ci-failure-result.js +90 -0
- package/dist/claude-headless.d.ts +7 -0
- package/dist/claude-headless.js +28 -0
- package/dist/cli.d.ts +17 -0
- package/dist/cli.js +55 -15
- package/dist/commands.d.ts +18 -0
- package/dist/commands.js +190 -85
- package/dist/constants.d.ts +32 -0
- package/dist/constants.js +3 -3
- package/dist/context.d.ts +24 -0
- package/dist/context.js +22 -6
- package/dist/dependency-readiness.d.ts +13 -0
- package/dist/dependency-readiness.js +80 -0
- package/dist/dispatch-scope.d.ts +18 -0
- package/dist/dispatch-scope.js +46 -0
- package/dist/done-evidence.d.ts +31 -0
- package/dist/done-evidence.js +216 -0
- package/dist/engine-config.d.ts +65 -0
- package/dist/engine-config.js +139 -0
- package/dist/fsops.d.ts +87 -0
- package/dist/fsops.js +117 -24
- package/dist/helpers.d.ts +15 -0
- package/dist/helpers.js +262 -45
- package/dist/intent-rules/admission.d.ts +10 -0
- package/dist/intent-rules/admission.js +35 -0
- package/dist/intent-rules/done-when-roles.d.ts +21 -0
- package/dist/intent-rules/done-when-roles.js +101 -0
- package/dist/intent-rules/hooks.d.ts +29 -0
- package/dist/intent-rules/hooks.js +23 -0
- package/dist/k3-codex-role.d.ts +20 -0
- package/dist/k3-codex-role.js +68 -0
- package/dist/manifest.d.ts +41 -0
- package/dist/manifest.js +59 -14
- package/dist/mcp.d.ts +18 -0
- package/dist/mcp.js +89 -39
- package/dist/opencode-result.d.ts +19 -0
- package/dist/opencode-result.js +136 -0
- package/dist/opencode-review-guard.d.ts +1 -0
- package/dist/opencode-review-guard.js +19 -0
- package/dist/opencode-session-export.d.ts +5 -0
- package/dist/opencode-session-export.js +28 -0
- package/dist/outcome-moves.d.ts +15 -0
- package/dist/outcome-moves.js +39 -0
- package/dist/planer-client.d.ts +214 -0
- package/dist/planer-client.js +351 -0
- package/dist/restart-recovery.d.ts +47 -0
- package/dist/restart-recovery.js +70 -0
- package/dist/review-source-packet.d.ts +17 -0
- package/dist/review-source-packet.js +44 -0
- package/dist/role-registry.d.ts +19 -0
- package/dist/role-registry.js +83 -0
- package/dist/role-results.d.ts +15 -0
- package/dist/role-results.js +42 -0
- package/dist/roles.d.ts +30 -0
- package/dist/roles.js +20 -16
- package/dist/seed-validation.d.ts +28 -0
- package/dist/seed-validation.js +294 -0
- package/dist/selection-result.d.ts +44 -0
- package/dist/selection-result.js +144 -0
- package/dist/sparse-policy.d.ts +8 -0
- package/dist/sparse-policy.js +49 -0
- package/dist/tester-identity.d.ts +11 -0
- package/dist/tester-identity.js +53 -0
- package/dist/worker-env.d.ts +5 -0
- package/dist/worker-env.js +34 -0
- package/docs/ENGINE-API.md +78 -0
- package/docs/ENGINE-CONFIG.md +115 -0
- package/docs/README.en.md +24 -15
- package/docs/README.md +26 -15
- package/docs/RELEASE-0.1.0.md +81 -8
- package/docs/RELEASE-0.1.1.md +110 -0
- package/helpers/ceo-client-launcher-mcp.mjs +36 -8
- package/helpers/client-spawn-session.sh +181 -0
- package/helpers/client-spawn.sh +825 -0
- package/package.json +9 -4
- package/roles/ceo-client.md +3 -2
- package/roles/manager-builder-client.md +18 -4
- package/roles/quality-officer-client.md +20 -6
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface NonWorkerItem {
|
|
2
|
+
index: number;
|
|
3
|
+
role: string;
|
|
4
|
+
excerpt: string;
|
|
5
|
+
}
|
|
6
|
+
/** Regex roli jako słowa z dosłownych form (wielkość liter ma znaczenie: „CR” nie łapie „cr-blocker.ts”). Dłuższe formy najpierw. */
|
|
7
|
+
export declare function roleMatcher(forms: readonly string[]): RegExp;
|
|
8
|
+
export type NonWorkerDoneWhen = (doneWhen: unknown) => NonWorkerItem[];
|
|
9
|
+
/** Reguła dla ról instancji: pozycje done_when wymagające cudzej czynności lub dowodu live. Nie-stringi pomija. */
|
|
10
|
+
export declare function nonWorkerDoneWhenFor(forms: readonly string[]): NonWorkerDoneWhen;
|
|
11
|
+
export interface NonWorkerRefusal {
|
|
12
|
+
reason: string;
|
|
13
|
+
index: number;
|
|
14
|
+
role: string;
|
|
15
|
+
excerpt: string;
|
|
16
|
+
doneWhenSha: string;
|
|
17
|
+
}
|
|
18
|
+
/** Odmowa dla done_when wymagającego cudzej czynności lub dowodu live; null = brak zastrzeżeń. */
|
|
19
|
+
export declare function nonWorkerRefusal(rule: NonWorkerDoneWhen, doneWhen: unknown): NonWorkerRefusal | null;
|
|
20
|
+
/** Sam powód odmowy (string) — jak `nonWorkerDoneWhenReason` w oryginale. */
|
|
21
|
+
export declare function nonWorkerDoneWhenReason(rule: NonWorkerDoneWhen, doneWhen: unknown): string | null;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// done_when karty = WYŁĄCZNIE to, co Worker spełnia sam w repo (test / diff / polecenie). Odbiór, dedup, admission, review/CI/land
|
|
2
|
+
// i realne użycie to sprawy koordynatora i pipeline (pola independent_receiver / intended_real_use), nigdy done_when:
|
|
3
|
+
// siewca przepisuje done_when do briefu jako warunek startu, a Worker bez kanału dostawy staje.
|
|
4
|
+
//
|
|
5
|
+
// Port maszynista/done-when-roles.ts z robisie-app (kopia: docs/zrodla/kompilator/done-when-roles.ts). Jedyna zmiana:
|
|
6
|
+
// zamknięta lista ról z oryginału (ROLE) jest budowana z form ról instancji w robisie.config.json (roles.*.forms).
|
|
7
|
+
// Reszta — luka słów, czasowniki, strona bierna, szyk przestawny, dowód live — bez zmian.
|
|
8
|
+
//
|
|
9
|
+
// Sprawdzian jest SKŁADNIOWY, nie słowny: samo wspomnienie roli („odpowiedź CR na B”, `recipientRole: "control-room"`)
|
|
10
|
+
// to legalne kryterium Workera; zatrzymujemy dopiero pozycję, w której rola WYKONUJE czynność (CR odbiera / potwierdza / porównuje /
|
|
11
|
+
// dostarcza ..., „odebrane przez CR”, „odbiera CR”). Heurystyka: zwykły język polski, więc rozpoznaje zamkniętą listę czasowników;
|
|
12
|
+
// reszta to znane fałszywe negatywy (otwarte ryzyko).
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
const WORD_START = String.raw `(?<![\p{L}\p{M}\p{N}_])`;
|
|
15
|
+
const WORD_END = String.raw `(?![\p{L}\p{M}\p{N}_])`;
|
|
16
|
+
/** Słowo luki między rolą a czynnością; „Worker” przerywa lukę (zmiana podmiotu). Interpunkcja kończy zdanie/człon, więc luka jej nie obejmuje. */
|
|
17
|
+
const GAP_WORD = String.raw `\s+(?!worker)[\p{L}\p{N}_/-]+`;
|
|
18
|
+
const MAX_GAP = 5;
|
|
19
|
+
/** Czasowniki czynności roli (os. 3. czas teraźniejszy/przyszły/przeszły + bezokolicznik). Tylko formy osobowe i bezokolicznik: „potwierdzony odbiór” to nie czynność. */
|
|
20
|
+
const ACTION_VERBS = [
|
|
21
|
+
'odbiera(?:ją)?|odbierze|odbiorą|odebra(?:ł|ła|li)|odbierać|odebrać',
|
|
22
|
+
'porównuje|porównują|porówna|porównają|porównał|porównała|porównali|porównać',
|
|
23
|
+
'dostarcza|dostarczają|dostarczy|dostarczą|dostarczył|dostarczyła|dostarczyli|dostarczyć',
|
|
24
|
+
'potwierdza|potwierdzają|potwierdzi|potwierdzą|potwierdził|potwierdziła|potwierdzili|potwierdzić',
|
|
25
|
+
'odczytuje|odczytują|odczyta|odczytają|odczytał|odczytała|odczytali|odczytać',
|
|
26
|
+
'zatwierdza|zatwierdzają|zatwierdzi|zatwierdzą|zatwierdził|zatwierdziła|zatwierdzili|zatwierdzić',
|
|
27
|
+
'akceptuje|akceptują|zaakceptuje|zaakceptują|zaakceptował|zaakceptowała|zaakceptowali|zaakceptować',
|
|
28
|
+
'weryfikuje|weryfikują|zweryfikuje|zweryfikują|zweryfikował|zweryfikowała|zweryfikowali|zweryfikować',
|
|
29
|
+
'waliduje|walidują|zwaliduje|zwalidują|zwalidował|zwalidowała|zwalidować',
|
|
30
|
+
'ocenia|oceniają|oceni|ocenią|ocenił|oceniła|ocenili|ocenić',
|
|
31
|
+
'sprawdza|sprawdzają|sprawdzi|sprawdzą|sprawdził|sprawdziła|sprawdzili|sprawdzić',
|
|
32
|
+
'decyduje|decydują|zdecyduje|zdecydują|zdecydował|zdecydowała|zdecydowali|zdecydować',
|
|
33
|
+
'rozstrzyga|rozstrzygają|rozstrzygnie|rozstrzygną|rozstrzygnął|rozstrzygnęła|rozstrzygnąć',
|
|
34
|
+
'rozlicza|rozliczają|rozliczy|rozliczą|rozliczył|rozliczyła|rozliczyli|rozliczyć',
|
|
35
|
+
'uzgadnia|uzgadniają|uzgodni|uzgodnią|uzgodnił|uzgodniła|uzgodnili|uzgodnić',
|
|
36
|
+
'wydaje|wydają|wyda|wydadzą|wydał|wydała|wydali|wydać',
|
|
37
|
+
'ratyfikuje|ratyfikują|ratyfikował|ratyfikowała|ratyfikować',
|
|
38
|
+
'otrzymuje|otrzymują|otrzyma|otrzymają|otrzymał|otrzymała|otrzymali|otrzymać',
|
|
39
|
+
'migruje|migrują|zmigruje|zmigrują',
|
|
40
|
+
'wykona|wykonają|wykonuje|wykonują|wykonał|wykonała|wykonali|wykonać',
|
|
41
|
+
].join('|');
|
|
42
|
+
/** Strona bierna z wykonawcą: „odebrane (osobno) przez CR”, „dostarczonej przez CR”, „odbiór przez Control Room”. */
|
|
43
|
+
const PASSIVE_STEMS = String.raw `(?:odebran|odbiór|odbior|zatwierdzon|potwierdzon|zweryfikowan|zaakceptowan|rozliczon|dostarczon|sprawdzon|ocenion|wykonan|odczytan|porównan|zwalidowan|uzgodnion)\p{L}*`;
|
|
44
|
+
/** Rola jest podmiotem: „CR (po aktywacji hosta) odbiera …”. Dopasowanie od końca roli. */
|
|
45
|
+
const ACTION_AFTER_ROLE = new RegExp(`^(?:${GAP_WORD}){0,${MAX_GAP}}?\\s+(?:${ACTION_VERBS})${WORD_END}`, 'iu');
|
|
46
|
+
/** Rola jest wykonawcą w stronie biernej. Dopasowanie do początku roli. */
|
|
47
|
+
const PASSIVE_BEFORE_ROLE = new RegExp(`${WORD_START}${PASSIVE_STEMS}(?:${GAP_WORD}){0,2}?\\s+przez\\s+$`, 'iu');
|
|
48
|
+
/** Szyk przestawny: „Zgodę wydaje właściciel”, „… odbiera CR”. Dopasowanie do początku roli. */
|
|
49
|
+
const ACTION_BEFORE_ROLE = new RegExp(`${WORD_START}(?:${ACTION_VERBS})${WORD_END}(?:${GAP_WORD}){0,1}?\\s+$`, 'iu');
|
|
50
|
+
const escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
51
|
+
/** Regex roli jako słowa z dosłownych form (wielkość liter ma znaczenie: „CR” nie łapie „cr-blocker.ts”). Dłuższe formy najpierw. */
|
|
52
|
+
export function roleMatcher(forms) {
|
|
53
|
+
const words = [...new Set(forms.map(form => form.trim()).filter(Boolean))].sort((a, b) => b.length - a.length);
|
|
54
|
+
if (!words.length)
|
|
55
|
+
throw new Error('reguła done_when: wymagana co najmniej jedna forma roli innej niż Worker');
|
|
56
|
+
return new RegExp(`${WORD_START}(?:${words.map(escape).join('|')})${WORD_END}`, 'gu');
|
|
57
|
+
}
|
|
58
|
+
function roleAction(text, roleAt) {
|
|
59
|
+
for (const match of text.matchAll(roleAt)) {
|
|
60
|
+
const start = match.index, end = start + match[0].length;
|
|
61
|
+
if (ACTION_AFTER_ROLE.test(text.slice(end, end + 200))
|
|
62
|
+
|| PASSIVE_BEFORE_ROLE.test(text.slice(Math.max(0, start - 100), start))
|
|
63
|
+
|| ACTION_BEFORE_ROLE.test(text.slice(Math.max(0, start - 60), start)))
|
|
64
|
+
return match[0];
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
/** Dowód działającego systemu należy do odbioru po landzie. Test opisujący taki przypadek nadal jest lokalnym kryterium. */
|
|
69
|
+
function liveAction(text) {
|
|
70
|
+
if (/^\s*(?:test|fixture|mock|symulacj)\p{L}*\s+(?:sprawdza|odtwarza|symuluje|weryfikuje)/iu.test(text))
|
|
71
|
+
return null;
|
|
72
|
+
return /(?:\bCI\b[^.;\n]{0,60}(?:exact[ -]SHA|(?:na|dla)\s+(?:tym\s+SHA|HEAD|kandydata))|(?:realn|rzeczywist)\p{L}*\s+(?:budzeni|wybudzeni)\p{L}*|(?:działa|wdrożon\p{L}*|potwierdzon\p{L}*)[^.;\n]{0,35}(?:na\s+produkcji|\bprod\b))/iu.test(text) ? 'pipeline/live' : null;
|
|
73
|
+
}
|
|
74
|
+
/** Reguła dla ról instancji: pozycje done_when wymagające cudzej czynności lub dowodu live. Nie-stringi pomija. */
|
|
75
|
+
export function nonWorkerDoneWhenFor(forms) {
|
|
76
|
+
const roleAt = roleMatcher(forms);
|
|
77
|
+
return doneWhen => {
|
|
78
|
+
const items = typeof doneWhen === 'string' ? [doneWhen] : Array.isArray(doneWhen) ? doneWhen : [];
|
|
79
|
+
return items.flatMap((item, index) => {
|
|
80
|
+
const role = typeof item === 'string' ? roleAction(item, roleAt) ?? liveAction(item) : null;
|
|
81
|
+
return role === null ? [] : [{ index, role, excerpt: String(item).replace(/\s+/g, ' ').slice(0, 90) }];
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** Odmowa dla done_when wymagającego cudzej czynności lub dowodu live; null = brak zastrzeżeń. */
|
|
86
|
+
export function nonWorkerRefusal(rule, doneWhen) {
|
|
87
|
+
const [first] = rule(doneWhen);
|
|
88
|
+
if (!first)
|
|
89
|
+
return null;
|
|
90
|
+
return {
|
|
91
|
+
reason: first.role === 'pipeline/live'
|
|
92
|
+
? `done_when[${first.index}]: dowód live — przenieś do effect_check / odbioru po landzie`
|
|
93
|
+
: `done_when[${first.index}]: akcja roli innej niż Worker (${first.role})`,
|
|
94
|
+
index: first.index, role: first.role, excerpt: first.excerpt,
|
|
95
|
+
doneWhenSha: createHash('sha256').update(JSON.stringify(doneWhen)).digest('hex'),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/** Sam powód odmowy (string) — jak `nonWorkerDoneWhenReason` w oryginale. */
|
|
99
|
+
export function nonWorkerDoneWhenReason(rule, doneWhen) {
|
|
100
|
+
return nonWorkerRefusal(rule, doneWhen)?.reason ?? null;
|
|
101
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type RolesConfig } from '../engine-config.js';
|
|
2
|
+
import { type NonWorkerItem } from './done-when-roles.js';
|
|
3
|
+
export type DoneWhenFinding = NonWorkerItem;
|
|
4
|
+
export interface AuthorityClass {
|
|
5
|
+
kind: string;
|
|
6
|
+
}
|
|
7
|
+
export interface MachineFieldProblem {
|
|
8
|
+
path: string;
|
|
9
|
+
expectedFormat: string;
|
|
10
|
+
}
|
|
11
|
+
export interface AuthorityInput {
|
|
12
|
+
id: string;
|
|
13
|
+
title: string;
|
|
14
|
+
details: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
export interface CompilerHooks {
|
|
17
|
+
nonWorkerDoneWhen(doneWhen: unknown): DoneWhenFinding[];
|
|
18
|
+
classifyAuthority(card: AuthorityInput): AuthorityClass;
|
|
19
|
+
machineFieldProblem(meta: Record<string, unknown>, projectId: string): MachineFieldProblem | null;
|
|
20
|
+
}
|
|
21
|
+
export declare function classifyAuthority(card: AuthorityInput): AuthorityClass;
|
|
22
|
+
export interface HookSettings {
|
|
23
|
+
roles: RolesConfig;
|
|
24
|
+
expectationProject?: string;
|
|
25
|
+
}
|
|
26
|
+
/** Domyślne reguły kompilatora dla ról i ustawień instancji. */
|
|
27
|
+
export declare function hooksFor(settings: HookSettings): CompilerHooks;
|
|
28
|
+
/** Reguły z domyślnymi (neutralnymi) rolami produktu. */
|
|
29
|
+
export declare const defaultHooks: CompilerHooks;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Reguły, które oryginał importował z repo robisie-app, przeniesione do pakietu:
|
|
2
|
+
// nonWorkerDoneWhen ← app/maszynista/done-when-roles (src/done-when-roles.ts; role z robisie.config.json)
|
|
3
|
+
// machineFieldProblem ← app/src/lib/planner/admission (src/admission.ts; projekt oczekiwań z konfiguracji)
|
|
4
|
+
// classifyAuthority ← app/maszynista/lib
|
|
5
|
+
// classifyAuthority: oryginał przy jawnym details.authority=auto|human zwraca dokładnie tę wartość (lib.ts:595–597), a kompilator
|
|
6
|
+
// odrzuca każdą inną wartość authority przed zapisem payloadu — więc dla każdej przyjętej intencji wynik jest ten sam co tutaj.
|
|
7
|
+
// Hooki można nadal podmienić przez API (`compileIntent(..., {hooks})`), ale domyślne reguły działają także w CLI.
|
|
8
|
+
import { machineFieldProblem as admissionProblem } from './admission.js';
|
|
9
|
+
import { DEFAULT_ROLES, nonWorkerRoleForms } from '../engine-config.js';
|
|
10
|
+
import { nonWorkerDoneWhenFor } from './done-when-roles.js';
|
|
11
|
+
export function classifyAuthority(card) {
|
|
12
|
+
return { kind: String(card.details.authority) };
|
|
13
|
+
}
|
|
14
|
+
/** Domyślne reguły kompilatora dla ról i ustawień instancji. */
|
|
15
|
+
export function hooksFor(settings) {
|
|
16
|
+
return {
|
|
17
|
+
nonWorkerDoneWhen: nonWorkerDoneWhenFor(nonWorkerRoleForms(settings.roles)),
|
|
18
|
+
classifyAuthority,
|
|
19
|
+
machineFieldProblem: (meta, projectId) => admissionProblem(meta, projectId, settings.expectationProject),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/** Reguły z domyślnymi (neutralnymi) rolami produktu. */
|
|
23
|
+
export const defaultHooks = hooksFor({ roles: DEFAULT_ROLES });
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type SpawnSyncOptionsWithStringEncoding, type SpawnSyncReturns } from "node:child_process";
|
|
2
|
+
type RoleBundle = {
|
|
3
|
+
disposition: string;
|
|
4
|
+
instructions: string;
|
|
5
|
+
execution: {
|
|
6
|
+
provider: string;
|
|
7
|
+
model: string;
|
|
8
|
+
effort: string;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
export interface K3CodexRoleOptions {
|
|
12
|
+
binary: string;
|
|
13
|
+
cwd: string;
|
|
14
|
+
env: NodeJS.ProcessEnv;
|
|
15
|
+
sandbox: "read-only" | "workspace-write";
|
|
16
|
+
timeoutMs: number;
|
|
17
|
+
}
|
|
18
|
+
/** Runs a prepared role once; process success is not a review verdict. */
|
|
19
|
+
export declare function runK3CodexRole(bundle: RoleBundle, options: K3CodexRoleOptions, spawn?: (binary: string, argv: readonly string[], options: SpawnSyncOptionsWithStringEncoding) => SpawnSyncReturns<string>): SpawnSyncReturns<string>;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
3
|
+
function requireObject(value, field) {
|
|
4
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
5
|
+
throw new Error(`K3 Codex: ${field} must be an object`);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
function requireText(value, field, absolute = false) {
|
|
9
|
+
if (typeof value !== "string" || !value.trim() || (absolute && !isAbsolute(value))) {
|
|
10
|
+
throw new Error(`K3 Codex: ${field} must be a nonempty ${absolute ? "absolute path" : "string"}`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function diagnostics(value) {
|
|
14
|
+
const read = (key) => {
|
|
15
|
+
try {
|
|
16
|
+
return typeof value === "object" && value !== null ? value[key] : null;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const status = read("status");
|
|
23
|
+
const signal = read("signal");
|
|
24
|
+
const code = read("code");
|
|
25
|
+
return `status=${typeof status === "number" && Number.isSafeInteger(status) ? status : null}; `
|
|
26
|
+
+ `signal=${signal === "SIGTERM" || signal === "SIGKILL" ? signal : null}; `
|
|
27
|
+
+ `code=${code === "ETIMEDOUT" || code === "ENOENT" || code === "EACCES" ? code : null}`;
|
|
28
|
+
}
|
|
29
|
+
/** Runs a prepared role once; process success is not a review verdict. */
|
|
30
|
+
export function runK3CodexRole(bundle, options, spawn = spawnSync) {
|
|
31
|
+
requireObject(bundle, "bundle");
|
|
32
|
+
requireObject(bundle.execution, "execution");
|
|
33
|
+
requireObject(options, "options");
|
|
34
|
+
requireObject(options.env, "env");
|
|
35
|
+
if (bundle.disposition !== "READY")
|
|
36
|
+
throw new Error("K3 Codex: disposition must be READY");
|
|
37
|
+
if (bundle.execution.provider !== "codex")
|
|
38
|
+
throw new Error("K3 Codex: provider must be codex");
|
|
39
|
+
requireText(bundle.instructions, "instructions");
|
|
40
|
+
requireText(bundle.execution.model, "model");
|
|
41
|
+
requireText(bundle.execution.effort, "effort");
|
|
42
|
+
requireText(options.binary, "binary", true);
|
|
43
|
+
requireText(options.cwd, "cwd", true);
|
|
44
|
+
requireText(options.env.HOME, "HOME");
|
|
45
|
+
requireText(options.env.PATH, "PATH");
|
|
46
|
+
requireText(options.env.CODEX_HOME, "CODEX_HOME", true);
|
|
47
|
+
if (options.sandbox !== "read-only" && options.sandbox !== "workspace-write") {
|
|
48
|
+
throw new Error("K3 Codex: sandbox must be read-only or workspace-write");
|
|
49
|
+
}
|
|
50
|
+
if (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs <= 0) {
|
|
51
|
+
throw new Error("K3 Codex: timeoutMs must be a positive safe integer");
|
|
52
|
+
}
|
|
53
|
+
const argv = ["exec", "--json", "--ephemeral", "--skip-git-repo-check", "--ignore-user-config", "--ignore-rules",
|
|
54
|
+
"--model", bundle.execution.model, "-c", `model_reasoning_effort=${JSON.stringify(bundle.execution.effort)}`,
|
|
55
|
+
"-C", options.cwd, "-s", options.sandbox, "-"];
|
|
56
|
+
let result;
|
|
57
|
+
try {
|
|
58
|
+
result = spawn(options.binary, argv, { input: bundle.instructions, encoding: "utf8", cwd: options.cwd,
|
|
59
|
+
env: options.env, timeout: options.timeoutMs, shell: false, killSignal: "SIGKILL", maxBuffer: 4 * 1024 * 1024 });
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
throw new Error(`K3 Codex process threw: ${diagnostics(error)}`);
|
|
63
|
+
}
|
|
64
|
+
if (result.status !== 0 || result.signal || result.error) {
|
|
65
|
+
throw new Error(`K3 Codex process failed: ${diagnostics(result)}${result.error ? `; ${diagnostics(result.error)}` : ""}`);
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type AgentId } from "./agents.js";
|
|
2
|
+
import type { RoleKind } from "./roles.js";
|
|
3
|
+
import { type Entry } from "./fsops.js";
|
|
4
|
+
/**
|
|
5
|
+
* The package's own directory (`$XDG_CONFIG_HOME/robisie`, default `~/.config/robisie`): key, manifest,
|
|
6
|
+
* backups and Codex role files. The package creates it and is its only owner — nothing else lives there,
|
|
7
|
+
* and uninstall still deletes only the files it recorded (never recursively).
|
|
8
|
+
*/
|
|
9
|
+
export interface Manifest {
|
|
10
|
+
packageVersion: string;
|
|
11
|
+
agents: AgentId[];
|
|
12
|
+
entries: Entry[];
|
|
13
|
+
/** Kept raw so doctor and uninstall can still inspect entries when the map is damaged. */
|
|
14
|
+
roleProviders?: unknown;
|
|
15
|
+
}
|
|
16
|
+
export declare const ROLE_IDS: readonly ["ceo", "builder", "quality"];
|
|
17
|
+
export type RoleProviders = Record<RoleKind, AgentId>;
|
|
18
|
+
export declare const ROLE_STEP = "U\u017Cyj pe\u0142nej jednolitej mapy: `npx robisie init --agent codex --role ceo=codex --role builder=codex --role quality=codex` (role: ceo, builder, quality; dostawcy: claude, codex, opencode).";
|
|
19
|
+
/** Legacy single-provider installs have one unambiguous assignment; explicit null stays null. */
|
|
20
|
+
export declare function roleProviders(manifest: Pick<Manifest, "agents" | "roleProviders">): RoleProviders | null;
|
|
21
|
+
export declare function clientPaths(dir: string): {
|
|
22
|
+
dir: string;
|
|
23
|
+
manifest: string;
|
|
24
|
+
key: string;
|
|
25
|
+
backups: string;
|
|
26
|
+
roles: string;
|
|
27
|
+
codexRoles: string;
|
|
28
|
+
};
|
|
29
|
+
/** Refuse symlinks at owned paths; ancestors such as XDG_CONFIG_HOME may be symlinks. */
|
|
30
|
+
export declare function refuseSymlink(path: string): void;
|
|
31
|
+
/** Ensure the package directory is private (0700) — parents keep their normal mode. */
|
|
32
|
+
export declare function ensurePrivateDir(dir: string): void;
|
|
33
|
+
/**
|
|
34
|
+
* Atomic private write: a fresh temp file created exclusively (O_EXCL, 0600) next to the target, then
|
|
35
|
+
* rename. Either the old or the new content is on disk — never half of it — and a symlink is never followed.
|
|
36
|
+
*/
|
|
37
|
+
export declare function writePrivate(dir: string, path: string, text: string): void;
|
|
38
|
+
export declare function readManifest(path: string): Manifest | null;
|
|
39
|
+
export declare function writeManifest(dir: string, manifest: Omit<Manifest, "packageVersion">): void;
|
|
40
|
+
export declare function writeKey(dir: string, key: string): void;
|
|
41
|
+
export declare function readKey(dir: string): string | null;
|
package/dist/manifest.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { closeSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeSync } from "node:fs";
|
|
1
|
+
import { chmodSync, closeSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeSync } from "node:fs";
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
3
|
import { basename, dirname, join } from "node:path";
|
|
4
4
|
import { PACKAGE_VERSION } from "./constants.js";
|
|
5
|
-
import { RobisieError, errorMessage } from "./context.js";
|
|
5
|
+
import { RobisieError, errorMessage, shellQuote } from "./context.js";
|
|
6
6
|
import { isAgentId } from "./agents.js";
|
|
7
|
+
import { freeCopyPath } from "./fsops.js";
|
|
7
8
|
export const ROLE_IDS = ["ceo", "builder", "quality"];
|
|
8
|
-
export const ROLE_STEP = "Użyj --role ceo=
|
|
9
|
+
export const ROLE_STEP = "Użyj pełnej jednolitej mapy: `npx robisie init --agent codex --role ceo=codex --role builder=codex --role quality=codex` (role: ceo, builder, quality; dostawcy: claude, codex, opencode).";
|
|
9
10
|
/** Legacy single-provider installs have one unambiguous assignment; explicit null stays null. */
|
|
10
11
|
export function roleProviders(manifest) {
|
|
11
12
|
if (!Object.hasOwn(manifest, "roleProviders")) {
|
|
@@ -21,7 +22,7 @@ export function roleProviders(manifest) {
|
|
|
21
22
|
const provider = value[role];
|
|
22
23
|
return Object.hasOwn(value, role) && typeof provider === "string" && isAgentId(provider);
|
|
23
24
|
})) {
|
|
24
|
-
throw new RobisieError("Mapa roleProviders jest uszkodzona lub niekompletna (ceo, builder, quality).", `Przywróć poprawną mapę roleProviders w manifest.json z kopii albo ustaw null. ${ROLE_STEP}`);
|
|
25
|
+
throw new RobisieError("Mapa roleProviders jest uszkodzona lub niekompletna (ceo, builder, quality).", `Przywróć poprawną mapę roleProviders w manifest.json z kopii albo ustaw wyłącznie pole roleProviders na null w manifest.json, zachowując agents i entries. ${ROLE_STEP}`);
|
|
25
26
|
}
|
|
26
27
|
return value;
|
|
27
28
|
}
|
|
@@ -48,23 +49,45 @@ function isSymlink(path) {
|
|
|
48
49
|
/** Refuse symlinks at owned paths; ancestors such as XDG_CONFIG_HOME may be symlinks. */
|
|
49
50
|
export function refuseSymlink(path) {
|
|
50
51
|
if (isSymlink(path)) {
|
|
51
|
-
|
|
52
|
+
let sourceAvailable = false;
|
|
53
|
+
let directorySource = null;
|
|
54
|
+
try {
|
|
55
|
+
const source = statSync(path);
|
|
56
|
+
sourceAvailable = source.isFile();
|
|
57
|
+
if (source.isDirectory())
|
|
58
|
+
directorySource = realpathSync(path);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error.code !== "ENOENT")
|
|
62
|
+
throw new RobisieError(`Nie mogę sprawdzić źródła ${path}: ${errorMessage(error)}`, `Sprawdź uprawnienia do ${path}.`);
|
|
63
|
+
}
|
|
64
|
+
const copy = freeCopyPath(path);
|
|
65
|
+
throw new RobisieError(`${path} jest dowiązaniem symbolicznym — nie korzystam z niego jako pliku ani katalogu robisie.`, directorySource
|
|
66
|
+
? `Zachowaj dowiązanie i skopiuj dostępny katalog: \`mv -n -- ${shellQuote(path)} ${shellQuote(copy)} && cp -R -- ${shellQuote(directorySource)} ${shellQuote(path)}\` (cel i istniejące kopie pozostają). Potem ponów poprzednie polecenie robisie w tym samym katalogu konfiguracji.`
|
|
67
|
+
: sourceAvailable
|
|
68
|
+
? `Zamień dowiązanie na zwykły plik: \`cp ${shellQuote(path)} ${shellQuote(copy)} && mv ${shellQuote(copy)} ${shellQuote(path)}\` (cel i istniejące kopie pozostają). Potem ponów poprzednie polecenie robisie w tym samym katalogu konfiguracji.`
|
|
69
|
+
: `Brak zwykłego pliku źródłowego. Przywróć ${path} jako zwykły plik lub katalog z poprawnej kopii tej samej instalacji, zachowując dowiązanie i jego cel. Bez dostępnego źródła nie można bezpiecznie kontynuować; zmiana katalogu konfiguracji nie naprawi starej ewidencji.`);
|
|
52
70
|
}
|
|
53
71
|
}
|
|
54
|
-
/**
|
|
72
|
+
/** Ensure the package directory is private (0700) — parents keep their normal mode. */
|
|
55
73
|
export function ensurePrivateDir(dir) {
|
|
56
74
|
refuseSymlink(dir);
|
|
57
75
|
try {
|
|
58
76
|
mkdirSync(dirname(dir), { recursive: true });
|
|
59
77
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
78
|
+
refuseSymlink(dir);
|
|
79
|
+
const stat = lstatSync(dir);
|
|
80
|
+
if (!stat.isDirectory()) {
|
|
81
|
+
throw new RobisieError(`${dir} nie jest katalogiem.`, `Przenieś ${dir} w inne miejsce i uruchom polecenie ponownie.`);
|
|
82
|
+
}
|
|
83
|
+
if ((stat.mode & 0o7777) !== 0o700)
|
|
84
|
+
chmodSync(dir, 0o700);
|
|
60
85
|
}
|
|
61
86
|
catch (error) {
|
|
87
|
+
if (error instanceof RobisieError)
|
|
88
|
+
throw error;
|
|
62
89
|
throw new RobisieError(`Nie mogę utworzyć katalogu ${dir}: ${errorMessage(error)}`, `Sprawdź uprawnienia do katalogu ${dirname(dir)}.`);
|
|
63
90
|
}
|
|
64
|
-
refuseSymlink(dir);
|
|
65
|
-
if (!lstatSync(dir).isDirectory()) {
|
|
66
|
-
throw new RobisieError(`${dir} nie jest katalogiem.`, `Przenieś ${dir} w inne miejsce i uruchom polecenie ponownie.`);
|
|
67
|
-
}
|
|
68
91
|
}
|
|
69
92
|
/**
|
|
70
93
|
* Atomic private write: a fresh temp file created exclusively (O_EXCL, 0600) next to the target, then
|
|
@@ -111,6 +134,26 @@ export function writePrivate(dir, path, text) {
|
|
|
111
134
|
throw new RobisieError(`Nie mogę zapisać ${path}: ${errorMessage(error)}`, `Sprawdź uprawnienia do katalogu ${dir}.`);
|
|
112
135
|
}
|
|
113
136
|
}
|
|
137
|
+
function isEntry(value) {
|
|
138
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
139
|
+
return false;
|
|
140
|
+
const entry = value;
|
|
141
|
+
if (typeof entry.path !== "string" || (entry.backup !== null && typeof entry.backup !== "string"))
|
|
142
|
+
return false;
|
|
143
|
+
switch (entry.kind) {
|
|
144
|
+
case "file":
|
|
145
|
+
return (entry.mode === null || typeof entry.mode === "number") &&
|
|
146
|
+
(!Object.hasOwn(entry, "fingerprint") || typeof entry.fingerprint === "string");
|
|
147
|
+
case "block":
|
|
148
|
+
return (entry.comment === "hash" || entry.comment === "html") && typeof entry.created === "boolean";
|
|
149
|
+
case "json":
|
|
150
|
+
return Array.isArray(entry.keyPath) && entry.keyPath.every((key) => typeof key === "string") &&
|
|
151
|
+
typeof entry.created === "boolean" && typeof entry.fingerprint === "string" &&
|
|
152
|
+
(entry.mode === null || typeof entry.mode === "number");
|
|
153
|
+
default:
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
114
157
|
export function readManifest(path) {
|
|
115
158
|
refuseSymlink(dirname(path));
|
|
116
159
|
refuseSymlink(path);
|
|
@@ -125,14 +168,14 @@ export function readManifest(path) {
|
|
|
125
168
|
}
|
|
126
169
|
try {
|
|
127
170
|
const parsed = JSON.parse(text);
|
|
128
|
-
if (!Array.isArray(parsed.entries) || !Array.isArray(parsed.agents))
|
|
171
|
+
if (!Array.isArray(parsed.entries) || !parsed.entries.every(isEntry) || !Array.isArray(parsed.agents))
|
|
129
172
|
throw new Error("brak pól");
|
|
130
173
|
const manifest = { packageVersion: String(parsed.packageVersion ?? "?"), agents: parsed.agents, entries: parsed.entries };
|
|
131
174
|
manifest.roleProviders = Object.hasOwn(parsed, "roleProviders") ? parsed.roleProviders : roleProviders(manifest);
|
|
132
175
|
return manifest;
|
|
133
176
|
}
|
|
134
177
|
catch {
|
|
135
|
-
throw new RobisieError(`Plik ${path} jest uszkodzony.`, `
|
|
178
|
+
throw new RobisieError(`Plik ${path} jest uszkodzony.`, `Przywróć ${path} z poprawnej kopii ewidencji tej samej instalacji, zachowując uszkodzony plik w osobnej kopii. Bez poprawnej kopii brakuje ewidencji potrzebnej do bezpiecznego init/uninstall — zatrzymaj się i skontaktuj się z robisie; nowy init jej nie odtworzy.`);
|
|
136
179
|
}
|
|
137
180
|
}
|
|
138
181
|
export function writeManifest(dir, manifest) {
|
|
@@ -150,7 +193,9 @@ export function readKey(dir) {
|
|
|
150
193
|
try {
|
|
151
194
|
return readFileSync(path, "utf8").trim() || null;
|
|
152
195
|
}
|
|
153
|
-
catch {
|
|
154
|
-
|
|
196
|
+
catch (error) {
|
|
197
|
+
if (error.code === "ENOENT")
|
|
198
|
+
return null;
|
|
199
|
+
throw new RobisieError(`Nie mogę odczytać pliku ${path} (błąd IO).`, `Sprawdź prawa odczytu pliku ${path}. Jeśli nie jest zwykłym plikiem, przywróć go z poprawnej kopii tej samej instalacji, zachowując istniejący plik lub katalog i jego zawartość w osobnej kopii. Potem wczytaj ROBISIE_KEY zgodnie z instrukcją instalacji i ponów \`npx robisie doctor\`.`);
|
|
155
200
|
}
|
|
156
201
|
}
|
package/dist/mcp.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Ctx } from "./context.js";
|
|
2
|
+
/** Major of a semver string, or null when it is not one. */
|
|
3
|
+
export declare function majorOf(version: string): number | null;
|
|
4
|
+
/** Throws a RobisieError when the server's contract MAJOR is not the one this package speaks. */
|
|
5
|
+
export declare function assertCompatible(serverVersion: string | null): void;
|
|
6
|
+
/** Public, unauthenticated: the contract version the server announces in /.well-known/mcp.json. */
|
|
7
|
+
export declare function fetchContractVersion(ctx: Ctx): Promise<string | null>;
|
|
8
|
+
export interface RpcResult {
|
|
9
|
+
result: unknown;
|
|
10
|
+
contractVersion: string | null;
|
|
11
|
+
}
|
|
12
|
+
export interface Connection {
|
|
13
|
+
serverName: string;
|
|
14
|
+
contractVersion: string | null;
|
|
15
|
+
projectCount: number;
|
|
16
|
+
}
|
|
17
|
+
/** initialize + list_projects: proves the key works end-to-end and reads the board. */
|
|
18
|
+
export declare function checkConnection(ctx: Ctx, key: string): Promise<Connection>;
|
package/dist/mcp.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CONTRACT_VERSION_HEADER, PACKAGE_NAME, PACKAGE_VERSION, SUPPORTED_CONTRACT_MAJOR, mcpUrl, wellKnownUrl } from "./constants.js";
|
|
2
|
-
import { RobisieError
|
|
2
|
+
import { RobisieError } from "./context.js";
|
|
3
3
|
const TIMEOUT_MS = 15_000;
|
|
4
|
-
const UPDATE_STEP = "
|
|
4
|
+
const UPDATE_STEP = "Zainstaluj z npm aktualną paczkę zgodną z kontraktem serwera; w katalogu instalacji wykonaj `npm i robisie@latest`, potem lokalne `npx robisie init` i `npx robisie doctor`.";
|
|
5
5
|
/** Major of a semver string, or null when it is not one. */
|
|
6
6
|
export function majorOf(version) {
|
|
7
7
|
const match = /^(\d+)\.\d+\.\d+/.exec(version.trim());
|
|
@@ -15,15 +15,15 @@ export function assertCompatible(serverVersion) {
|
|
|
15
15
|
const major = majorOf(serverVersion);
|
|
16
16
|
if (major === SUPPORTED_CONTRACT_MAJOR)
|
|
17
17
|
return;
|
|
18
|
-
const
|
|
19
|
-
throw new RobisieError(`Wersja kontraktu serwera (${serverVersion}) nie pasuje do paczki ${PACKAGE_NAME} ${PACKAGE_VERSION} (obsługuje ${SUPPORTED_CONTRACT_MAJOR}.x).`,
|
|
18
|
+
const older = major !== null && major < SUPPORTED_CONTRACT_MAJOR;
|
|
19
|
+
throw new RobisieError(`Wersja kontraktu serwera (${serverVersion}) nie pasuje do paczki ${PACKAGE_NAME} ${PACKAGE_VERSION} (obsługuje ${SUPPORTED_CONTRACT_MAJOR}.x).`, older ? "Serwer jest starszy niż paczka. Zainstaluj z npm wydanie zgodne z kontraktem serwera: `npm i robisie@<wersja-zgodna-z-kontraktem-serwera>` (zastąp placeholder numerem zgodnego wydania), potem lokalne `npx robisie init` i `npx robisie doctor`." : UPDATE_STEP);
|
|
20
20
|
}
|
|
21
21
|
async function request(ctx, url, init) {
|
|
22
22
|
try {
|
|
23
|
-
return await ctx.fetch(url, { ...init, signal: AbortSignal.timeout(TIMEOUT_MS) });
|
|
23
|
+
return await ctx.fetch(url, { ...init, redirect: "error", signal: AbortSignal.timeout(TIMEOUT_MS) });
|
|
24
24
|
}
|
|
25
|
-
catch
|
|
26
|
-
throw new RobisieError(`Brak połączenia z ${new URL(url).host}
|
|
25
|
+
catch {
|
|
26
|
+
throw new RobisieError(`Brak połączenia z ${new URL(url).host}.`, "Sprawdź internet (albo proxy/firewall) i spróbuj ponownie.");
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
/** Public, unauthenticated: the contract version the server announces in /.well-known/mcp.json. */
|
|
@@ -41,37 +41,68 @@ export async function fetchContractVersion(ctx) {
|
|
|
41
41
|
return null;
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
+
function isRecord(value) {
|
|
45
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
46
|
+
}
|
|
47
|
+
function nonEmptyString(value) {
|
|
48
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
49
|
+
}
|
|
50
|
+
function formatError() {
|
|
51
|
+
return new RobisieError("Serwer robisie odpowiedział w nieznanym formacie.", UPDATE_STEP);
|
|
52
|
+
}
|
|
44
53
|
/** Streamable HTTP answers either plain JSON or an SSE stream; pick our response out of either. */
|
|
45
54
|
async function readRpc(res, id) {
|
|
46
|
-
const
|
|
47
|
-
const candidates = [];
|
|
48
|
-
if ((res.headers.get("content-type") ?? "").includes("text/event-stream")) {
|
|
49
|
-
for (const event of text.split(/\r?\n\r?\n/)) {
|
|
50
|
-
const data = event
|
|
51
|
-
.split(/\r?\n/)
|
|
52
|
-
.filter((line) => line.startsWith("data:"))
|
|
53
|
-
.map((line) => line.slice(5).trimStart())
|
|
54
|
-
.join("\n");
|
|
55
|
-
if (data)
|
|
56
|
-
candidates.push(data);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
60
|
-
candidates.push(text);
|
|
61
|
-
}
|
|
62
|
-
for (const candidate of candidates) {
|
|
55
|
+
const matching = (candidate) => {
|
|
63
56
|
try {
|
|
64
57
|
const parsed = JSON.parse(candidate);
|
|
65
58
|
for (const message of Array.isArray(parsed) ? parsed : [parsed]) {
|
|
66
|
-
if (
|
|
59
|
+
if (isRecord(message) && message.id === id)
|
|
67
60
|
return message;
|
|
68
61
|
}
|
|
69
62
|
}
|
|
70
63
|
catch {
|
|
71
64
|
// not JSON — skip this event
|
|
72
65
|
}
|
|
66
|
+
return null;
|
|
67
|
+
};
|
|
68
|
+
let reader;
|
|
69
|
+
try {
|
|
70
|
+
if (!(res.headers.get("content-type") ?? "").includes("text/event-stream"))
|
|
71
|
+
return matching(await res.text());
|
|
72
|
+
if (!res.body)
|
|
73
|
+
return null;
|
|
74
|
+
reader = res.body.getReader();
|
|
75
|
+
const decoder = new TextDecoder();
|
|
76
|
+
let buffer = "";
|
|
77
|
+
while (true) {
|
|
78
|
+
const { done, value } = await reader.read();
|
|
79
|
+
if (done)
|
|
80
|
+
return null; // EOF never completes an unterminated SSE event.
|
|
81
|
+
buffer += decoder.decode(value, { stream: true });
|
|
82
|
+
let boundary;
|
|
83
|
+
while ((boundary = /\r?\n\r?\n/.exec(buffer)) !== null) {
|
|
84
|
+
const event = buffer.slice(0, boundary.index);
|
|
85
|
+
buffer = buffer.slice(boundary.index + boundary[0].length);
|
|
86
|
+
const data = event.split(/\r?\n/)
|
|
87
|
+
.filter((line) => line.startsWith("data:"))
|
|
88
|
+
.map((line) => line.slice(5).trimStart())
|
|
89
|
+
.join("\n");
|
|
90
|
+
const message = matching(data);
|
|
91
|
+
if (message)
|
|
92
|
+
return message;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
throw new RobisieError("Nie mogę odczytać odpowiedzi serwera robisie.", "Sprawdź połączenie i uruchom ponownie `npx robisie doctor`.");
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
if (reader) {
|
|
101
|
+
// Cancellation may never settle; release ownership without delaying the RPC result.
|
|
102
|
+
void reader.cancel().catch(() => { });
|
|
103
|
+
reader.releaseLock();
|
|
104
|
+
}
|
|
73
105
|
}
|
|
74
|
-
return null;
|
|
75
106
|
}
|
|
76
107
|
async function rpc(ctx, key, id, method, params) {
|
|
77
108
|
const res = await request(ctx, mcpUrl(ctx.origin), {
|
|
@@ -93,10 +124,13 @@ async function rpc(ctx, key, id, method, params) {
|
|
|
93
124
|
if (!res.ok)
|
|
94
125
|
throw new RobisieError(`Serwer robisie odpowiedział błędem ${res.status}.`, "Spróbuj ponownie za kilka minut; jeśli błąd zostaje, napisz do robisie.");
|
|
95
126
|
const message = await readRpc(res, id);
|
|
96
|
-
if (!message)
|
|
97
|
-
throw
|
|
98
|
-
if (message
|
|
99
|
-
|
|
127
|
+
if (!message || message.jsonrpc !== "2.0" || ("result" in message) === ("error" in message))
|
|
128
|
+
throw formatError();
|
|
129
|
+
if ("error" in message) {
|
|
130
|
+
if (!isRecord(message.error) || !Number.isInteger(message.error.code) || typeof message.error.message !== "string")
|
|
131
|
+
throw formatError();
|
|
132
|
+
throw new RobisieError("Serwer robisie zwrócił błąd RPC.", "Spróbuj ponownie; jeśli błąd zostaje, uruchom lokalne `npx robisie doctor` w katalogu instalacji.");
|
|
133
|
+
}
|
|
100
134
|
return { result: message.result, contractVersion };
|
|
101
135
|
}
|
|
102
136
|
/** initialize + list_projects: proves the key works end-to-end and reads the board. */
|
|
@@ -106,22 +140,38 @@ export async function checkConnection(ctx, key) {
|
|
|
106
140
|
capabilities: {},
|
|
107
141
|
clientInfo: { name: PACKAGE_NAME, version: PACKAGE_VERSION },
|
|
108
142
|
});
|
|
109
|
-
|
|
143
|
+
if (!isRecord(init.result) || !isRecord(init.result.serverInfo) || !nonEmptyString(init.result.serverInfo.name)
|
|
144
|
+
|| !nonEmptyString(init.result.protocolVersion) || !isRecord(init.result.capabilities))
|
|
145
|
+
throw formatError();
|
|
146
|
+
const serverName = init.result.serverInfo.name;
|
|
110
147
|
const list = await rpc(ctx, key, 2, "tools/call", { name: "list_projects", arguments: {} });
|
|
111
148
|
const result = list.result;
|
|
112
|
-
if (result
|
|
113
|
-
throw
|
|
114
|
-
|
|
149
|
+
if (!isRecord(result) || (result.isError !== undefined && typeof result.isError !== "boolean"))
|
|
150
|
+
throw formatError();
|
|
151
|
+
if (result.isError)
|
|
152
|
+
throw new RobisieError("Narzędzie list_projects zwróciło błąd.", "Spróbuj ponownie; jeśli błąd zostaje, napisz do robisie.");
|
|
153
|
+
if (!Array.isArray(result.content))
|
|
154
|
+
throw formatError();
|
|
155
|
+
const content = result.content.find((item) => isRecord(item) && item.type === "text");
|
|
156
|
+
if (!isRecord(content) || typeof content.text !== "string")
|
|
157
|
+
throw formatError();
|
|
158
|
+
let parsed;
|
|
115
159
|
try {
|
|
116
|
-
|
|
117
|
-
projectCount = Array.isArray(parsed) ? parsed.length : 0;
|
|
160
|
+
parsed = JSON.parse(content.text);
|
|
118
161
|
}
|
|
119
162
|
catch {
|
|
120
|
-
|
|
163
|
+
throw formatError();
|
|
121
164
|
}
|
|
165
|
+
if (!Array.isArray(parsed) || !parsed.every((project) => isRecord(project)
|
|
166
|
+
&& nonEmptyString(project.id) && nonEmptyString(project.name)))
|
|
167
|
+
throw formatError();
|
|
168
|
+
if (init.contractVersion)
|
|
169
|
+
assertCompatible(init.contractVersion);
|
|
170
|
+
if (list.contractVersion)
|
|
171
|
+
assertCompatible(list.contractVersion);
|
|
122
172
|
return {
|
|
123
|
-
serverName
|
|
173
|
+
serverName,
|
|
124
174
|
contractVersion: init.contractVersion ?? list.contractVersion,
|
|
125
|
-
projectCount,
|
|
175
|
+
projectCount: parsed.length,
|
|
126
176
|
};
|
|
127
177
|
}
|