@schlenkr/ragents 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 +2 -1
- package/apps/server/src/config-file.ts +2 -1
- package/apps/server/src/host-record.ts +31 -0
- package/apps/server/src/profile-target.ts +32 -0
- package/package.json +2 -2
- package/plugins/ragents.reference/walkthroughs.ts +10 -10
- package/ragents.config.core.ts +0 -1
- package/ragents.config.showcase.ts +64 -0
- package/scripts/agent/agent-cli.ts +65 -47
- package/scripts/package/build-package.ts +2 -2
- package/scripts/package/ragents.mjs +10 -0
- package/scripts/remote/connect.ts +26 -3
- package/scripts/remote/start.ts +2 -25
package/README.md
CHANGED
|
@@ -22,7 +22,8 @@ npm install -g @schlenkr/ragents
|
|
|
22
22
|
RAGENTS_TOKEN=<token> ragents connect https://ragents.example.com
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
- `start <profile|path>` - start a profile shipped with the package (`core`, `developer
|
|
25
|
+
- `start <profile|path>` - start a profile shipped with the package (`core`, `developer`,
|
|
26
|
+
`showcase`), your
|
|
26
27
|
own `ragents.config.<name>.ts` anywhere on disk, or a stand that `connect` already fetched,
|
|
27
28
|
without asking the server. The web interface is built for that set of plugins when it is
|
|
28
29
|
missing or was last built for another profile or host version; the result goes to
|
|
@@ -51,8 +51,9 @@ const resolveUserToken = (
|
|
|
51
51
|
if (!isEnvironmentReference(raw) || typeof raw.name !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(raw.name)) {
|
|
52
52
|
throw new Error(`${location}.token braucht env("ENV_NAME"); ein Token steht nie im Klartext in der Profildatei`);
|
|
53
53
|
}
|
|
54
|
+
// Ein Benutzer meldet sich weiter mit seinem Passwort an; ohne gesetzte Variable hat er nur keinen persönlichen Token.
|
|
54
55
|
const token = environment[raw.name];
|
|
55
|
-
if (!token)
|
|
56
|
+
if (!token) return undefined;
|
|
56
57
|
// $ und ! sind tabu, weil der Verbraucher den Token als API-Schlüssel mit Vorlagen- und Befehlssyntax auswertet.
|
|
57
58
|
if (token.length < MIN_TOKEN_LENGTH || token.length > MAX_TOKEN_LENGTH || !/^[\x21-\x7e]+$/.test(token) || /[$!]/.test(token)) {
|
|
58
59
|
throw new Error(`${location}.token (${raw.name}) braucht ${MIN_TOKEN_LENGTH} bis ${MAX_TOKEN_LENGTH} druckbare ASCII-Zeichen ohne Leerzeichen, $ und !`);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface HostRecord {
|
|
5
|
+
readonly profile: string;
|
|
6
|
+
readonly url: string;
|
|
7
|
+
readonly pid: number;
|
|
8
|
+
readonly log: string;
|
|
9
|
+
readonly startedAt: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Der gemerkte Host eines Profils; ragents run und ragents start schreiben dieselbe Datei, stop --host liest sie. */
|
|
13
|
+
export const hostRecordFile = (dataDirectory: string): string => path.join(dataDirectory, "host.json");
|
|
14
|
+
|
|
15
|
+
export const readHostRecord = (dataDirectory: string): HostRecord | undefined => {
|
|
16
|
+
const file = hostRecordFile(dataDirectory);
|
|
17
|
+
if (!statSync(file, { throwIfNoEntry: false })?.isFile()) return undefined;
|
|
18
|
+
const parsed = JSON.parse(readFileSync(file, "utf8")) as Partial<HostRecord>;
|
|
19
|
+
// Eine PID unter 1 wäre für process.kill die ganze Prozessgruppe; sie darf nie aus dieser Datei kommen.
|
|
20
|
+
if (typeof parsed.url !== "string" || !Number.isInteger(parsed.pid) || (parsed.pid ?? 0) < 1) throw new Error(`${file} nennt keine Adresse oder keine PID des Hosts`);
|
|
21
|
+
return parsed as HostRecord;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const writeHostRecord = (dataDirectory: string, record: HostRecord): void => {
|
|
25
|
+
mkdirSync(dataDirectory, { recursive: true });
|
|
26
|
+
writeFileSync(hostRecordFile(dataDirectory), `${JSON.stringify(record, null, 2)}\n`);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const removeHostRecord = (dataDirectory: string): void => {
|
|
30
|
+
rmSync(hostRecordFile(dataDirectory), { force: true });
|
|
31
|
+
};
|
|
@@ -2,7 +2,9 @@ import { statSync } from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import { isEnvironmentReference } from "./config-definition.js";
|
|
5
|
+
import { profileFileName } from "./config-file.js";
|
|
5
6
|
import { defaultDataDirectory } from "./data-directory.js";
|
|
7
|
+
import { hostRoot } from "./host-version.js";
|
|
6
8
|
|
|
7
9
|
export interface ProfileTarget {
|
|
8
10
|
readonly profile: string;
|
|
@@ -12,6 +14,29 @@ export interface ProfileTarget {
|
|
|
12
14
|
readonly baseUrl: string;
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
export interface LocalProfile {
|
|
18
|
+
readonly profile: string;
|
|
19
|
+
readonly profileFile: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const PROFILE_FILE = /^ragents\.config\.([a-z0-9]+(?:-[a-z0-9]+)*)\.ts$/;
|
|
23
|
+
|
|
24
|
+
/** pnpm und der bin-Befehl führen die Skripte mit dem Arbeitsverzeichnis apps/server aus; ein relativer Pfad gilt trotzdem ab dem Aufrufer. */
|
|
25
|
+
export const callerDirectory = (): string => process.env.RAGENTS_CWD ?? process.env.INIT_CWD ?? process.cwd();
|
|
26
|
+
|
|
27
|
+
/** Ein Profil ist ein Name neben dem Host (Checkout wie Paket) oder ein Pfad zu einer ragents.config.<profil>.ts an beliebiger Stelle. */
|
|
28
|
+
export const localProfile = (selection: string, root = hostRoot(), caller = callerDirectory()): LocalProfile | undefined => {
|
|
29
|
+
if (!selection.includes("/") && !selection.endsWith(".ts")) {
|
|
30
|
+
const file = path.join(root, profileFileName(selection));
|
|
31
|
+
return statSync(file, { throwIfNoEntry: false })?.isFile() ? { profile: selection, profileFile: file } : undefined;
|
|
32
|
+
}
|
|
33
|
+
const profileFile = path.resolve(caller, selection);
|
|
34
|
+
const match = PROFILE_FILE.exec(path.basename(profileFile));
|
|
35
|
+
if (!match) throw new Error(`Eine Profildatei heißt ragents.config.<profil>.ts, nicht ${path.basename(profileFile)}`);
|
|
36
|
+
if (!statSync(profileFile, { throwIfNoEntry: false })?.isFile()) throw new Error(`Die Profildatei fehlt: ${profileFile}`);
|
|
37
|
+
return { profile: match[1]!, profileFile };
|
|
38
|
+
};
|
|
39
|
+
|
|
15
40
|
/** Port und Datenordner eines Profils, wie der Server sie später selbst bestimmt: Umgebung vor Profildatei vor Vorgabe. */
|
|
16
41
|
export const readProfileTarget = async (profile: string, profileFile: string, environment = process.env): Promise<ProfileTarget> => {
|
|
17
42
|
if (!statSync(profileFile, { throwIfNoEntry: false })?.isFile()) throw new Error(`Die Profildatei fehlt: ${profileFile}`);
|
|
@@ -23,3 +48,10 @@ export const readProfileTarget = async (profile: string, profileFile: string, en
|
|
|
23
48
|
const dataDirectory = path.resolve(environment.DATA_DIR ?? fromFile ?? defaultDataDirectory(profile));
|
|
24
49
|
return { profile, profileFile, port, dataDirectory, baseUrl: `http://localhost:${port}` };
|
|
25
50
|
};
|
|
51
|
+
|
|
52
|
+
/** Die eine Auflösung für alle Befehle: Name oder Pfad zum Profil, danach Port und Datenordner wie beim Server. */
|
|
53
|
+
export const selectProfileTarget = async (selection: string, root = hostRoot(), environment = process.env): Promise<ProfileTarget> => {
|
|
54
|
+
const own = localProfile(selection, root);
|
|
55
|
+
if (!own) throw new Error(`Die Profildatei fehlt: ${path.join(root, profileFileName(selection))}`);
|
|
56
|
+
return readProfileTarget(own.profile, own.profileFile, environment);
|
|
57
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schlenkr/ragents",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "RAgents-Host: Server, Engine, Plugins und Werkzeuge ohne Quell-Checkout",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"fsharp",
|
|
12
12
|
"vscode"
|
|
13
13
|
],
|
|
14
|
-
"homepage": "https://github.
|
|
14
|
+
"homepage": "https://schlenkr.github.io/RAgents/",
|
|
15
15
|
"bugs": {
|
|
16
16
|
"url": "https://github.com/SchlenkR/RAgents/issues"
|
|
17
17
|
},
|
|
@@ -5,7 +5,7 @@ export const referenceWalkthroughs = [
|
|
|
5
5
|
description: "Die Mini-App bleibt als Kachel sichtbar; ihr Besitzer ist auch ohne eigene Kachel erreichbar.",
|
|
6
6
|
tags: ["Anwendungsfall", "Konzeptdemo", "Kachelfläche"],
|
|
7
7
|
steps: [
|
|
8
|
-
"Im Profil
|
|
8
|
+
"Im Profil showcase Sammelboard einrichten öffnen und den Leitfaden abschließen. Erwartung: Die Liste steht als Kachel auf der Fläche; ihr Besitzer hat keine eigene Kachel.",
|
|
9
9
|
"In der Kopfzeile Actors öffnen und den Namen des Listenbesitzers wählen. Erwartung: Rechts öffnen sich dessen Chat und Details, ohne die Kachelaufteilung zu ändern.",
|
|
10
10
|
"Den Besitzer aus der Kopfzeile auf die Kachel der Liste ziehen und rechts andocken. Erwartung: Seine Kachel erscheint neben der Liste; die Mini-App bleibt bedienbar und behält ihre Einträge.",
|
|
11
11
|
"Die neue Kachel über das Kreuz in ihrer Kopfzeile wieder entfernen und die App erneut bedienen. Erwartung: Actor und Funktionen arbeiten weiter; sein Name bleibt in der Kopfzeile erreichbar.",
|
|
@@ -17,7 +17,7 @@ export const referenceWalkthroughs = [
|
|
|
17
17
|
description: "Kacheln lassen sich persönlich umstellen und wieder auf die Programmvorgabe zurücksetzen.",
|
|
18
18
|
tags: ["Anwendungsfall", "Konzeptdemo", "Kachelfläche"],
|
|
19
19
|
steps: [
|
|
20
|
-
"Im Profil
|
|
20
|
+
"Im Profil showcase den Einstieg Gesprächsrunde einrichten ausführen und den Aufbau abwarten. Die vom Run gesetzte Aufteilung ansehen.",
|
|
21
21
|
"Eine Kachel an ihrer Kopfzeile fassen und am oberen Rand einer anderen Kachel andocken. Erwartung: Die Aufteilung ändert sich sofort; alle Gesprächspartner arbeiten weiter.",
|
|
22
22
|
"Die Trennlinie zwischen zwei Kacheln verschieben und denselben Run im selben Browser neu laden. Erwartung: Die persönliche Aufteilung bleibt erhalten; die Actors und die Programmanordnung sind unverändert.",
|
|
23
23
|
"In der Statusleiste Programmvorgabe übernehmen wählen. Erwartung: Die Fläche zeigt wieder die Aufteilung des Runs; der Knopf verschwindet, bis wieder eine eigene Änderung vorliegt.",
|
|
@@ -29,7 +29,7 @@ export const referenceWalkthroughs = [
|
|
|
29
29
|
description: "Der übergeordnete Koordinator liest vorhandene Runs, während die aktuelle Arbeitsfläche geöffnet bleibt.",
|
|
30
30
|
tags: ["Anwendungsfall", "Konzeptdemo", "Globaler Koordinator"],
|
|
31
31
|
steps: [
|
|
32
|
-
"Im Profil
|
|
32
|
+
"Im Profil showcase zwei kurze Unterhaltungen zu einer Leseliste und einem Wochenplan anlegen und ihre Antworten abwarten.",
|
|
33
33
|
"In die Eingabe Globaler Koordinator in der Kopfzeile klicken und damit seinen Verlauf öffnen. Um eine knappe Übersicht der beiden Unterhaltungen mit ihrem jeweiligen Arbeitsstand bitten.",
|
|
34
34
|
"Die Run-Liste über die Übersichtsecke oder Cmd+I auf macOS beziehungsweise Ctrl+I öffnen, beide Runs auswählen und die Antwort mit ihren tatsächlichen Unterhaltungen vergleichen. Erwartung: Der globale Chat kann beide Journale berücksichtigen; ein Run-Wechsel erhält seinen eigenen Verlauf.",
|
|
35
35
|
"Einen ungesendeten Entwurf oben eingeben, Escape drücken und den Verlauf wieder öffnen. Erwartung: Das Dropdown schließt ohne Stopp; Entwurf, aktueller Run und globales Gespräch bleiben erhalten.",
|
|
@@ -126,7 +126,7 @@ export const referenceWalkthroughs = [
|
|
|
126
126
|
description: "Eine TypeScript-Prüfung macht den zugehörigen Sprachserver in der Prozessanzeige sichtbar.",
|
|
127
127
|
tags: ["Anwendungsfall", "Konzeptdemo", "Prozessanzeige"],
|
|
128
128
|
steps: [
|
|
129
|
-
"Im Profil
|
|
129
|
+
"Im Profil showcase den Skill-Einstieg Fehler in einer Terminliste finden ausführen. Voraussetzung ist ein verfügbarer TypeScript-Sprachserver; eine fehlende Voraussetzung muss als Fehler gemeldet werden.",
|
|
130
130
|
"Nach dem Öffnen des Sprachservers die gemeinsame Kopfzeile des Runs ansehen. Erwartung: Der verwaltete Sprachserver erscheint als zu diesem Run gehörender Prozess.",
|
|
131
131
|
"Einen anderen Run öffnen und zurückwechseln. Erwartung: Die Prozessanzeige folgt dem ausgewählten Run und ist kein gemeinsames Verzeichnis aller Rechnerprozesse.",
|
|
132
132
|
"Die Prozessanzeige dient der Beobachtung. Sie bietet keinen Beenden-Knopf; fehlende Betriebssystemrechte oder Werkzeuge werden als sichtbarer Fehler gemeldet.",
|
|
@@ -138,7 +138,7 @@ export const referenceWalkthroughs = [
|
|
|
138
138
|
description: "Ein zeitlich begrenzter Demo-Webprozess erscheint mit seinem erkannten Port in der Kopfzeile.",
|
|
139
139
|
tags: ["Anwendungsfall", "Konzeptdemo", "Prozessanzeige"],
|
|
140
140
|
steps: [
|
|
141
|
-
"In einem eigenen
|
|
141
|
+
"In einem eigenen showcase-Run den Koordinator bitten, einen kleinen Node-Demoprozess im Run-Arbeitsverzeichnis vorzubereiten: Er liefert nur einen neutralen Begrüßungstext, bindet ausschließlich 127.0.0.1 auf einem freien Port und beendet sich nach zwei Minuten selbst. Keine zusätzlichen Pakete installieren und keinen Produktserver ersetzen.",
|
|
142
142
|
"Den vorbereiteten Demoprozess starten lassen und bei laufender Ausführung die Kopfzeile ansehen. Erwartung: Der Prozess erscheint mit einem Link auf seinen erkannten offenen Port.",
|
|
143
143
|
"Den Port-Link nur auf demselben Rechner öffnen. Erwartung: Die lokale Demoantwort erscheint; bei einem entfernten Server ist dessen Loopback-Adresse nicht die Adresse des Browsers.",
|
|
144
144
|
"Das vorgesehene automatische Ende abwarten. Erwartung: Der Eintrag verschwindet nach der nächsten Prozessaktualisierung. Run-Stopp oder das Schließen eines Tabs werden hier nicht als Beenden eines abgesetzten Dienstes versprochen.",
|
|
@@ -150,7 +150,7 @@ export const referenceWalkthroughs = [
|
|
|
150
150
|
description: "Ein Run schreibt einen kleinen Text; der Dateireiter zeigt Inhalt und spätere Änderung ohne eigenen Editor.",
|
|
151
151
|
tags: ["Anwendungsfall", "Konzeptdemo", "Dateien und Arbeitsverzeichnis"],
|
|
152
152
|
steps: [
|
|
153
|
-
"In einem neuen
|
|
153
|
+
"In einem neuen showcase-Run den Koordinator bitten, im Run-Arbeitsverzeichnis die Datei reading-list.md mit drei neutralen Lesethemen anzulegen.",
|
|
154
154
|
"Den Reiter Dateien öffnen, das Arbeitsverzeichnis aufklappen und reading-list.md auswählen. Erwartung: Der geschriebene Text erscheint als Vorschau.",
|
|
155
155
|
"Im Chat ein viertes Thema ergänzen lassen und zur weiterhin geöffneten Vorschau zurückkehren. Erwartung: Die Dateiänderung wird im Dateibrowser sichtbar.",
|
|
156
156
|
"Erwartung: Der Dateireiter bleibt eine Leseansicht. Änderungen erfolgen durch den beauftragten Actor und dessen Dateiwerkzeuge, nicht durch einen eingebauten Editor oder Löschknopf.",
|
|
@@ -162,8 +162,8 @@ export const referenceWalkthroughs = [
|
|
|
162
162
|
description: "Zwei unabhängige Arbeitsverzeichnisse enthalten eigene Dateien mit demselben Namen.",
|
|
163
163
|
tags: ["Anwendungsfall", "Konzeptdemo", "Dateien und Arbeitsverzeichnis"],
|
|
164
164
|
steps: [
|
|
165
|
-
"Einen
|
|
166
|
-
"Einen zweiten
|
|
165
|
+
"Einen showcase-Run Leseliste anlegen und darin notes.md mit dem Inhalt Lesen vorbereiten schreiben lassen.",
|
|
166
|
+
"Einen zweiten showcase-Run Wochenplan anlegen und dort ebenfalls notes.md schreiben lassen, diesmal mit dem Inhalt Woche planen. Keine Datei zwischen Runs kopieren.",
|
|
167
167
|
"In beiden Runs nacheinander den Reiter Dateien öffnen und notes.md lesen. Erwartung: Der gleiche Dateiname zeigt den jeweils eigenen Inhalt des Run-Arbeitsverzeichnisses.",
|
|
168
168
|
"Nur im Wochenplan eine Zeile ergänzen lassen und beide Vorschauen erneut prüfen. Erwartung: Die Datei des Leselisten-Runs ist unverändert.",
|
|
169
169
|
],
|
|
@@ -174,7 +174,7 @@ export const referenceWalkthroughs = [
|
|
|
174
174
|
description: "Eine abgeschlossene Actor-Funktion bleibt beim regulären Neustart desselben Profils erhalten.",
|
|
175
175
|
tags: ["Anwendungsfall", "Konzeptdemo", "Wiederherstellung nach Neustart"],
|
|
176
176
|
steps: [
|
|
177
|
-
"Im Profil
|
|
177
|
+
"Im Profil showcase Sammelboard einrichten öffnen und den Leitfaden abschließen. Einen eindeutig erkennbaren Eintrag hinzufügen und die bestätigte Aktualisierung der Liste abwarten.",
|
|
178
178
|
"Alle laufenden Arbeiten abschließen lassen. Den lokalen Server selbst über den für diese Installation verwendeten Startweg beenden und mit demselben Profil und Datenverzeichnis neu starten. Keine Daten löschen und keine zweite Serverinstanz parallel starten.",
|
|
179
179
|
"Die Anwendung erneut öffnen und denselben Run auswählen. Erwartung: Gespräch, Mini-App und bestätigter Listeneintrag sind wieder vorhanden.",
|
|
180
180
|
"Die Liste auf doppelte Einträge prüfen. Erwartung: Der Neustart stellt gespeicherten Zustand wieder her, führt die bereits abgeschlossene Hinzufügeaktion aber nicht erneut aus. Das Journal allein ersetzt keine vollständige Sicherung der übrigen Run-Dateien.",
|
|
@@ -186,7 +186,7 @@ export const referenceWalkthroughs = [
|
|
|
186
186
|
description: "Normale Unterhaltung und globaler Koordinator behalten ihre jeweils eigene Geschichte.",
|
|
187
187
|
tags: ["Anwendungsfall", "Konzeptdemo", "Wiederherstellung nach Neustart"],
|
|
188
188
|
steps: [
|
|
189
|
-
"In einem normalen
|
|
189
|
+
"In einem normalen showcase-Run drei Lernziele besprechen und im globalen Chat eine knappe Übersicht dieses Runs anfordern. Beide Antworten vollständig abwarten und die globale Modellwahl merken.",
|
|
190
190
|
"Den lokalen Server selbst über den vorhandenen Startweg neu starten, mit demselben Profil und Datenverzeichnis. Keine Dateien entfernen oder zwischen Installationen übertragen.",
|
|
191
191
|
"Den normalen Run und anschließend den globalen Chat öffnen. Erwartung: Beide bisherigen Verläufe und die globale Modellwahl sind erhalten.",
|
|
192
192
|
"Im normalen Run um den nächsten Schritt zu den Lernzielen bitten. Die neue Antwort als neue Arbeit behandeln; der Neustart hat alte Modellaufrufe nicht wiederholt. Ein beim Neustart noch offener Turn wäre unterbrochen abgeschlossen worden und wird nicht automatisch erneut ausgeführt.",
|
package/ragents.config.core.ts
CHANGED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Schaufenster: das Profil core und zusätzlich die mitgelieferten Beispiele aus ragents.reference.
|
|
2
|
+
import { env, provisioned, type RAgentsConfig } from "./apps/server/src/config-definition.js";
|
|
3
|
+
|
|
4
|
+
export const config = {
|
|
5
|
+
host: {
|
|
6
|
+
PORT: 4713,
|
|
7
|
+
PRODUCT_PROFILE: "showcase",
|
|
8
|
+
COMPACTION_MODEL: "google/gemma-4-26b-a4b-it",
|
|
9
|
+
PRODUCT_ID: "ragents-showcase",
|
|
10
|
+
PRODUCT_TITLE: "RAgents Showcase",
|
|
11
|
+
PLUGINS: [
|
|
12
|
+
"ragents.orchestration",
|
|
13
|
+
"ragents.workspace",
|
|
14
|
+
"ragents.product",
|
|
15
|
+
"ragents.overseer",
|
|
16
|
+
"ragents.activity",
|
|
17
|
+
"ragents.processes",
|
|
18
|
+
"ragents.documents",
|
|
19
|
+
"ragents.browser",
|
|
20
|
+
"ragents.ask",
|
|
21
|
+
"ragents.todo",
|
|
22
|
+
"ragents.watch",
|
|
23
|
+
"ragents.transcript",
|
|
24
|
+
"ragents.actor-programs",
|
|
25
|
+
"ragents.reference",
|
|
26
|
+
"ragents.lsp-roslyn",
|
|
27
|
+
"ragents.lsp-fsharp",
|
|
28
|
+
"ragents.lsp-typescript",
|
|
29
|
+
"ragents.model-relay",
|
|
30
|
+
"ragents.profile-distribution",
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
"ragents.product": {
|
|
35
|
+
OPENROUTER_API_KEY: env("OPENROUTER_API_KEY"),
|
|
36
|
+
AGENT_MODEL: "z-ai/glm-5.3-flash",
|
|
37
|
+
AGENT_MODELS: [
|
|
38
|
+
"deepseek/deepseek-v4-flash-0731",
|
|
39
|
+
"deepseek/deepseek-v4-pro-0813",
|
|
40
|
+
"qwen/qwen3.8-max",
|
|
41
|
+
"z-ai/glm-5.3",
|
|
42
|
+
"z-ai/glm-5.3-flash",
|
|
43
|
+
"qwen/qwen3.8-flash",
|
|
44
|
+
"moonshotai/kimi-k3",
|
|
45
|
+
"minimax/minimax-m3",
|
|
46
|
+
"qwen/qwen3.8-27b",
|
|
47
|
+
"moonshotai/kimi-k2.7-code",
|
|
48
|
+
],
|
|
49
|
+
AGENT_COORDINATOR_MODEL: "z-ai/glm-5.3-flash",
|
|
50
|
+
AGENT_COORDINATOR_THINKING: "high",
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
// Modell-Relay: Aliasse für andere RAgents-Server, die ihre Modelle von hier beziehen.
|
|
54
|
+
"ragents.model-relay": {
|
|
55
|
+
RELAY_MODELS: ["showcase-standard=openrouter/z-ai/glm-5.3-flash", "showcase-coordinator=openrouter/deepseek/deepseek-v4-flash-0731"],
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
"ragents.lsp-roslyn": {
|
|
59
|
+
ROSLYN_LANGUAGE_SERVER: provisioned("ragents.lsp-roslyn", "roslyn/Microsoft.CodeAnalysis.LanguageServer.dll"),
|
|
60
|
+
},
|
|
61
|
+
"ragents.lsp-fsharp": {
|
|
62
|
+
FSHARP_LANGUAGE_SERVER: provisioned("ragents.lsp-fsharp", "fsautocomplete/fsautocomplete.dll"),
|
|
63
|
+
},
|
|
64
|
+
} as const satisfies RAgentsConfig;
|
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
import "../../apps/server/src/host-resolution.ts";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
|
-
import { closeSync, mkdirSync, openSync, readFileSync,
|
|
5
|
+
import { closeSync, mkdirSync, openSync, readFileSync, statSync } from "node:fs";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { coreContracts } from "../../apps/server/src/api/contracts.ts";
|
|
9
|
-
import {
|
|
9
|
+
import { hostRecordFile, readHostRecord, removeHostRecord, writeHostRecord } from "../../apps/server/src/host-record.ts";
|
|
10
10
|
import { hostRoot } from "../../apps/server/src/host-version.ts";
|
|
11
|
-
import {
|
|
11
|
+
import { callerDirectory, selectProfileTarget, type ProfileTarget } from "../../apps/server/src/profile-target.ts";
|
|
12
12
|
import { RpcClient } from "../../apps/web/src/rpc/client.ts";
|
|
13
|
+
import { RpcError } from "../../packages/ragents/src/rpc/protocol.ts";
|
|
13
14
|
import { WORKSPACE_BINDING_OPTION_ID } from "../../plugins/ragents.workspace/contract.ts";
|
|
14
15
|
import { JournalReader, journalLines, readJournal, RUN_ID_PATTERN, type JournalEvent } from "./journal.ts";
|
|
15
16
|
|
|
@@ -18,6 +19,10 @@ const HOST_START_TIMEOUT_MS = 120_000;
|
|
|
18
19
|
const HOST_STOP_TIMEOUT_MS = 20_000;
|
|
19
20
|
const POLL_INTERVAL_MS = 400;
|
|
20
21
|
const HEALTH_TIMEOUT_MS = 2_000;
|
|
22
|
+
const CHAT_READY_TIMEOUT_MS = 60_000;
|
|
23
|
+
|
|
24
|
+
/** Vorgabeprofil der Agentenbefehle; RAGENTS_PROFILE gilt für alle Befehle derselben Shell. */
|
|
25
|
+
export const defaultProfile = (): string => process.env.RAGENTS_PROFILE ?? DEFAULT_PROFILE;
|
|
21
26
|
|
|
22
27
|
export const usage = (): string => `Verwendung: ragents <befehl> [argumente]
|
|
23
28
|
|
|
@@ -31,11 +36,13 @@ export const usage = (): string => `Verwendung: ragents <befehl> [argumente]
|
|
|
31
36
|
stop <run> [--profile <p>] den laufenden Turn abbrechen
|
|
32
37
|
stop --host [--profile <p>] den gemerkten Host beenden
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
<p> ist ein Profilname neben dem Host oder der Pfad zu einer ragents.config.<profil>.ts an
|
|
40
|
+
beliebiger Stelle; ohne --profile gilt RAGENTS_PROFILE, sonst ${DEFAULT_PROFILE}. Exit-Code:
|
|
41
|
+
0 fertig, 2 abgebrochen, 1 fehlgeschlagen oder Verbindungsproblem. Die letzte Zeile auf stdout
|
|
42
|
+
ist "run: <id>". Die Adresse kommt aus <Datenordner>/host.json, sonst aus RAGENTS_URL, sonst aus
|
|
43
|
+
host.PORT des Profils; RAGENTS_TOKEN geht als Bearer-Token mit, falls das Profil eine Anmeldung
|
|
44
|
+
verlangt. Der so gestartete Host baut die Oberfläche nicht - ein Agent braucht sie nicht; mit
|
|
45
|
+
Oberfläche startet ragents start <profil>.`;
|
|
39
46
|
|
|
40
47
|
export type AgentCommand =
|
|
41
48
|
| { readonly kind: "run"; readonly profile: string; readonly folder: string; readonly text: string; readonly entry: string | undefined; readonly json: boolean }
|
|
@@ -76,7 +83,7 @@ const scan = (argv: readonly string[], allowed: readonly string[]): Flags => {
|
|
|
76
83
|
index += 1;
|
|
77
84
|
}
|
|
78
85
|
return {
|
|
79
|
-
profile: values.get("--profile") ??
|
|
86
|
+
profile: values.get("--profile") ?? defaultProfile(),
|
|
80
87
|
entry: values.get("--entry"),
|
|
81
88
|
json: switches.has("--json"),
|
|
82
89
|
tools: switches.has("--tools"),
|
|
@@ -125,35 +132,7 @@ export const parseArguments = (argv: readonly string[]): AgentCommand => {
|
|
|
125
132
|
throw new Error(command ? `Unbekannter Befehl: ${command}\n\n${usage()}` : usage());
|
|
126
133
|
};
|
|
127
134
|
|
|
128
|
-
export
|
|
129
|
-
readonly profile: string;
|
|
130
|
-
readonly url: string;
|
|
131
|
-
readonly pid: number;
|
|
132
|
-
readonly log: string;
|
|
133
|
-
readonly startedAt: string;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export const hostRecordFile = (dataDirectory: string): string => path.join(dataDirectory, "host.json");
|
|
137
|
-
|
|
138
|
-
export const readHostRecord = (dataDirectory: string): HostRecord | undefined => {
|
|
139
|
-
const file = hostRecordFile(dataDirectory);
|
|
140
|
-
if (!statSync(file, { throwIfNoEntry: false })?.isFile()) return undefined;
|
|
141
|
-
const parsed = JSON.parse(readFileSync(file, "utf8")) as Partial<HostRecord>;
|
|
142
|
-
// Eine PID unter 1 wäre für process.kill die ganze Prozessgruppe; sie darf nie aus dieser Datei kommen.
|
|
143
|
-
if (typeof parsed.url !== "string" || !Number.isInteger(parsed.pid) || (parsed.pid ?? 0) < 1) throw new Error(`${file} nennt keine Adresse oder keine PID des Hosts`);
|
|
144
|
-
return parsed as HostRecord;
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
export const writeHostRecord = (dataDirectory: string, record: HostRecord): void => {
|
|
148
|
-
mkdirSync(dataDirectory, { recursive: true });
|
|
149
|
-
writeFileSync(hostRecordFile(dataDirectory), `${JSON.stringify(record, null, 2)}\n`);
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
export const loadProfile = (profile: string, root = hostRoot()): Promise<ProfileTarget> =>
|
|
153
|
-
readProfileTarget(profile, path.join(root, profileFileName(profile)));
|
|
154
|
-
|
|
155
|
-
/** pnpm und der bin-Befehl führen dieses Skript mit dem Arbeitsverzeichnis apps/server aus; ein relativer Ordner gilt trotzdem ab dem Aufrufer. */
|
|
156
|
-
export const callerDirectory = (): string => process.env.RAGENTS_CWD ?? process.env.INIT_CWD ?? process.cwd();
|
|
135
|
+
export const loadProfile = (selection: string, root = hostRoot()): Promise<ProfileTarget> => selectProfileTarget(selection, root);
|
|
157
136
|
|
|
158
137
|
const delay = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
|
|
159
138
|
|
|
@@ -239,6 +218,34 @@ const client = (baseUrl: string): RpcClient => {
|
|
|
239
218
|
});
|
|
240
219
|
};
|
|
241
220
|
|
|
221
|
+
export const LOGIN_REQUIRED = "Das Profil verlangt eine Anmeldung; setze RAGENTS_TOKEN auf den persönlichen Token deines Benutzers "
|
|
222
|
+
+ "(im Profil `token: env(...)`).";
|
|
223
|
+
|
|
224
|
+
/** Ein 401 ist keine Fehlermeldung des Servers wert, sondern der Hinweis auf den persönlichen Token des Profils. */
|
|
225
|
+
export const withLoginHint = async <T>(call: () => Promise<T>): Promise<T> => {
|
|
226
|
+
try {
|
|
227
|
+
return await call();
|
|
228
|
+
} catch (error) {
|
|
229
|
+
if (error instanceof RpcError && error.status === 401) throw new Error(`${LOGIN_REQUIRED} ${error.message}`);
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/** Ein Einstieg richtet den Run erst ein und bestimmt dabei seinen Chatpartner; bis dahin lehnt der Server die Nachricht ab. */
|
|
235
|
+
const sendWhenChatReady = async (rpc: RpcClient, runId: string, text: string): Promise<void> => {
|
|
236
|
+
const deadline = Date.now() + CHAT_READY_TIMEOUT_MS;
|
|
237
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
238
|
+
try {
|
|
239
|
+
await rpc.call(coreContracts.chat.send, { runId, text });
|
|
240
|
+
return;
|
|
241
|
+
} catch (error) {
|
|
242
|
+
if (!(error instanceof RpcError) || error.domainCode !== "actor-chat-unsupported" || Date.now() >= deadline) throw error;
|
|
243
|
+
if (attempt === 0) note("== Der Einstieg richtet den Run ein; der Auftrag wartet auf seinen Chatpartner.");
|
|
244
|
+
await delay(POLL_INTERVAL_MS);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
242
249
|
export type TurnOutcome = "completed" | "interrupted" | "failed";
|
|
243
250
|
|
|
244
251
|
export const EXIT_CODES: Readonly<Record<TurnOutcome, number>> = { completed: 0, interrupted: 2, failed: 1 };
|
|
@@ -351,11 +358,18 @@ const runCommand = async (command: Extract<AgentCommand, { kind: "run" }>, write
|
|
|
351
358
|
const baseUrl = await ensureHost(target);
|
|
352
359
|
const rpc = client(baseUrl);
|
|
353
360
|
const runId = randomUUID();
|
|
354
|
-
await rpc.call(coreContracts.startOptions.
|
|
355
|
-
|
|
361
|
+
const options = await withLoginHint(() => rpc.call(coreContracts.startOptions.list, { runId }));
|
|
362
|
+
if (options.some((option) => option.id === WORKSPACE_BINDING_OPTION_ID)) {
|
|
363
|
+
await withLoginHint(() => rpc.call(coreContracts.startOptions.select, { runId, optionId: WORKSPACE_BINDING_OPTION_ID, value: { kind: "path", path: folder } }));
|
|
364
|
+
note(`== Run ${runId} auf ${folder} (${baseUrl})`);
|
|
365
|
+
} else {
|
|
366
|
+
note(`== Run ${runId} (${baseUrl}); das Profil ${target.profile} kennt ${WORKSPACE_BINDING_OPTION_ID} nicht und legt seinen Arbeitsbereich selbst an, ${folder} bleibt ungebunden.`);
|
|
367
|
+
}
|
|
356
368
|
const reader = new JournalReader(target.dataDirectory, runId);
|
|
357
|
-
if (command.entry) await rpc.call(coreContracts.chat.start, { runId, entry: command.entry });
|
|
358
|
-
await
|
|
369
|
+
if (command.entry) await withLoginHint(() => rpc.call(coreContracts.chat.start, { runId, entry: command.entry }));
|
|
370
|
+
await withLoginHint(() => command.entry
|
|
371
|
+
? sendWhenChatReady(rpc, runId, command.text)
|
|
372
|
+
: rpc.call(coreContracts.chat.send, { runId, text: command.text }).then(() => undefined));
|
|
359
373
|
const outcome = await follow({ reader, baseUrl, text: command.text.trim(), json: command.json, write });
|
|
360
374
|
write(`run: ${runId}`);
|
|
361
375
|
return EXIT_CODES[outcome];
|
|
@@ -368,7 +382,7 @@ const sendCommand = async (command: Extract<AgentCommand, { kind: "send" }>, wri
|
|
|
368
382
|
const rpc = client(baseUrl);
|
|
369
383
|
const reader = new JournalReader(target.dataDirectory, command.runId);
|
|
370
384
|
reader.next();
|
|
371
|
-
await rpc.call(coreContracts.chat.send, { runId: command.runId, text: command.text });
|
|
385
|
+
await withLoginHint(() => rpc.call(coreContracts.chat.send, { runId: command.runId, text: command.text }));
|
|
372
386
|
const outcome = await follow({ reader, baseUrl, text: command.text.trim(), json: command.json, write });
|
|
373
387
|
write(`run: ${command.runId}`);
|
|
374
388
|
return EXIT_CODES[outcome];
|
|
@@ -385,7 +399,7 @@ const journalCommand = async (command: Extract<AgentCommand, { kind: "journal" }
|
|
|
385
399
|
const stopCommand = async (command: Extract<AgentCommand, { kind: "stop" }>): Promise<number> => {
|
|
386
400
|
const target = await loadProfile(command.profile);
|
|
387
401
|
const baseUrl = await addressOf(target);
|
|
388
|
-
await client(baseUrl).call(coreContracts.chat.stop, { runId: command.runId });
|
|
402
|
+
await withLoginHint(() => client(baseUrl).call(coreContracts.chat.stop, { runId: command.runId }));
|
|
389
403
|
note(`== Turn von ${command.runId} abgebrochen`);
|
|
390
404
|
return 0;
|
|
391
405
|
};
|
|
@@ -404,7 +418,7 @@ const stopHostCommand = async (command: Extract<AgentCommand, { kind: "stop-host
|
|
|
404
418
|
const noted = readHostRecord(target.dataDirectory);
|
|
405
419
|
if (!noted) throw new Error(`Für das Profil ${command.profile} ist kein Host gemerkt (${hostRecordFile(target.dataDirectory)} fehlt).`);
|
|
406
420
|
if (!alive(noted.pid)) {
|
|
407
|
-
|
|
421
|
+
removeHostRecord(target.dataDirectory);
|
|
408
422
|
note(`== Der gemerkte Host (PID ${noted.pid}) läuft nicht mehr; der Vermerk ist weg.`);
|
|
409
423
|
return 0;
|
|
410
424
|
}
|
|
@@ -415,7 +429,7 @@ const stopHostCommand = async (command: Extract<AgentCommand, { kind: "stop-host
|
|
|
415
429
|
process.kill(noted.pid, "SIGKILL");
|
|
416
430
|
await delay(500);
|
|
417
431
|
}
|
|
418
|
-
|
|
432
|
+
removeHostRecord(target.dataDirectory);
|
|
419
433
|
note(`== Host ${noted.url} (PID ${noted.pid}) beendet`);
|
|
420
434
|
return 0;
|
|
421
435
|
};
|
|
@@ -430,10 +444,14 @@ export const execute = async (command: AgentCommand, write: LineWriter = toStdou
|
|
|
430
444
|
|
|
431
445
|
const main = async (): Promise<number> => {
|
|
432
446
|
const argv = process.argv.slice(2);
|
|
433
|
-
if (argv
|
|
447
|
+
if (argv[0] === "--help" || argv[0] === "help") {
|
|
434
448
|
console.log(usage());
|
|
435
449
|
return 0;
|
|
436
450
|
}
|
|
451
|
+
if (argv.length === 0) {
|
|
452
|
+
console.error(usage());
|
|
453
|
+
return 1;
|
|
454
|
+
}
|
|
437
455
|
return execute(parseArguments(argv));
|
|
438
456
|
};
|
|
439
457
|
|
|
@@ -22,7 +22,7 @@ const SKIPPED_IN_PACKAGE = /^scripts\/.*\.test\.tsx?$/;
|
|
|
22
22
|
const packagePath = (packageRoot: string, file: string): string => path.relative(packageRoot, file).split(path.sep).join("/");
|
|
23
23
|
|
|
24
24
|
/** Die neutralen Profile des Repositories; ragents start fährt sie aus dem Paket wie aus dem Checkout. */
|
|
25
|
-
export const PACKAGED_PROFILES = ["core", "developer"] as const;
|
|
25
|
+
export const PACKAGED_PROFILES = ["core", "developer", "showcase"] as const;
|
|
26
26
|
|
|
27
27
|
export const PACKAGE_LICENSE = "PolyForm-Shield-1.0.0";
|
|
28
28
|
export const PACKAGE_AUTHOR = "Ronald Schlenker";
|
|
@@ -217,7 +217,7 @@ export const buildPackage = async (target: string, root = repositoryRoot): Promi
|
|
|
217
217
|
version: readPackageVersion(root),
|
|
218
218
|
description: "RAgents-Host: Server, Engine, Plugins und Werkzeuge ohne Quell-Checkout",
|
|
219
219
|
keywords: [...PACKAGE_KEYWORDS],
|
|
220
|
-
homepage:
|
|
220
|
+
homepage: "https://schlenkr.github.io/RAgents/",
|
|
221
221
|
bugs: { url: `${REPOSITORY_URL}/issues` },
|
|
222
222
|
license: PACKAGE_LICENSE,
|
|
223
223
|
author: PACKAGE_AUTHOR,
|
|
@@ -29,11 +29,21 @@ const usage = `Verwendung: ragents <befehl> [argumente]
|
|
|
29
29
|
geholten Stand starten; baut die Oberfläche bei Bedarf
|
|
30
30
|
provision [<profil>|--workspace] Werkzeuge der Plugins holen
|
|
31
31
|
workspace-client <server-url> diesen Rechner als Arbeitsplatz anmelden
|
|
32
|
+
--help, help diese Verwendung
|
|
33
|
+
|
|
34
|
+
run, send, journal und stop nehmen mit --profile <profil|pfad> ein anderes Profil: einen Namen
|
|
35
|
+
neben dem Host oder den Pfad zu einer eigenen ragents.config.<profil>.ts. RAGENTS_PROFILE setzt
|
|
36
|
+
dasselbe für alle Befehle einer Shell; verlangt das Profil eine Anmeldung, gehört der
|
|
37
|
+
persönliche Token des Benutzers in RAGENTS_TOKEN.
|
|
32
38
|
`;
|
|
33
39
|
|
|
34
40
|
const main = () => {
|
|
35
41
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
36
42
|
const [command, ...rest] = process.argv.slice(2);
|
|
43
|
+
if (command === "--help" || command === "help") {
|
|
44
|
+
console.log(usage);
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
37
47
|
const script = command ? commands[command] : undefined;
|
|
38
48
|
if (!script) {
|
|
39
49
|
console.error(command ? `Unbekannter Befehl: ${command}\n\n${usage}` : usage);
|
|
@@ -7,6 +7,7 @@ import path from "node:path";
|
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { extract as extractTar } from "tar";
|
|
9
9
|
import { ragentsDataRoot } from "../../apps/server/src/data-directory.ts";
|
|
10
|
+
import { removeHostRecord, writeHostRecord } from "../../apps/server/src/host-record.ts";
|
|
10
11
|
import { hostRoot, readHostPackage, readHostVersion } from "../../apps/server/src/host-version.ts";
|
|
11
12
|
import { readProfileTarget } from "../../apps/server/src/profile-target.ts";
|
|
12
13
|
import { ensureWebBuild } from "../../apps/server/src/web-build.ts";
|
|
@@ -187,8 +188,13 @@ export const hostScript = (script: string, args: readonly string[] = []): { file
|
|
|
187
188
|
cwd: path.join(hostRoot(), "apps/server"),
|
|
188
189
|
});
|
|
189
190
|
|
|
190
|
-
const run = (
|
|
191
|
+
const run = (
|
|
192
|
+
command: { file: string; args: readonly string[]; cwd: string },
|
|
193
|
+
env: NodeJS.ProcessEnv,
|
|
194
|
+
started?: (pid: number) => void,
|
|
195
|
+
): Promise<number> => new Promise((resolve, reject) => {
|
|
191
196
|
const child = spawn(command.file, [...command.args], { cwd: command.cwd, env, stdio: "inherit" });
|
|
197
|
+
if (child.pid) started?.(child.pid);
|
|
192
198
|
const forward = (signal: NodeJS.Signals) => () => child.kill(signal);
|
|
193
199
|
const onInterrupt = forward("SIGINT");
|
|
194
200
|
const onTerminate = forward("SIGTERM");
|
|
@@ -233,9 +239,21 @@ export interface StartedProfile {
|
|
|
233
239
|
readonly port: number | undefined;
|
|
234
240
|
}
|
|
235
241
|
|
|
242
|
+
/** ragents run und ragents start merken denselben Host im Datenordner des Profils; ein Port 0 sucht sich seinen Port selbst und lässt sich nicht merken. */
|
|
243
|
+
export const noteHost = (profile: string, dataDirectory: string, port: number): ((pid: number) => void) | undefined =>
|
|
244
|
+
port > 0 ? (pid: number): void => writeHostRecord(dataDirectory, {
|
|
245
|
+
profile,
|
|
246
|
+
url: `http://localhost:${port}`,
|
|
247
|
+
pid,
|
|
248
|
+
log: path.join(dataDirectory, "logs", "server.log"),
|
|
249
|
+
startedAt: new Date().toISOString(),
|
|
250
|
+
}) : undefined;
|
|
251
|
+
|
|
236
252
|
/** Provisioniert die Werkzeuge des Profils, baut sein Web falls nötig und startet den Server damit. */
|
|
237
253
|
export const startProfile = async (started: StartedProfile): Promise<number> => {
|
|
238
|
-
const
|
|
254
|
+
const target = await readProfileTarget(started.profile, started.profileFile);
|
|
255
|
+
const dataDirectory = started.dataDirectory ?? target.dataDirectory;
|
|
256
|
+
const port = started.port ?? target.port;
|
|
239
257
|
const env: NodeJS.ProcessEnv = {
|
|
240
258
|
...process.env,
|
|
241
259
|
PRODUCT_PROFILE: started.profile,
|
|
@@ -254,7 +272,12 @@ export const startProfile = async (started: StartedProfile): Promise<number> =>
|
|
|
254
272
|
log: (line) => console.log(line),
|
|
255
273
|
});
|
|
256
274
|
console.log(`== Server starten (Web ${webDirectory}, Daten ${dataDirectory})`);
|
|
257
|
-
|
|
275
|
+
const note = noteHost(started.profile, dataDirectory, port);
|
|
276
|
+
try {
|
|
277
|
+
return await run(hostScript("apps/server/src/main.ts", started.port !== undefined ? ["--port", String(started.port)] : []), { ...env, WEB_DIST_DIR: webDirectory }, note);
|
|
278
|
+
} finally {
|
|
279
|
+
if (note) removeHostRecord(dataDirectory);
|
|
280
|
+
}
|
|
258
281
|
};
|
|
259
282
|
|
|
260
283
|
/** Startet einen Stand, den connect geholt hat, mit dem Web aus dem Cache. */
|
package/scripts/remote/start.ts
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
// Der Hook löst @aicontainer/* auch für eine Profildatei außerhalb des Hosts auf; im Paket gibt es dort kein node_modules.
|
|
2
2
|
import "../../apps/server/src/host-resolution.ts";
|
|
3
|
-
import { statSync } from "node:fs";
|
|
4
3
|
import { readFile, readdir } from "node:fs/promises";
|
|
5
4
|
import path from "node:path";
|
|
6
5
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { profileFileName } from "../../apps/server/src/config-file.ts";
|
|
8
6
|
import { ragentsDataRoot } from "../../apps/server/src/data-directory.ts";
|
|
9
|
-
import {
|
|
10
|
-
import { callerDirectory } from "../agent/agent-cli.ts";
|
|
7
|
+
import { localProfile } from "../../apps/server/src/profile-target.ts";
|
|
11
8
|
import { startCached, startProfile, type CachedProfile } from "./connect.ts";
|
|
12
9
|
|
|
13
10
|
const usage = (): string => `Verwendung: ragents start <profil|pfad> [--port <n>]
|
|
14
|
-
Startet ein Profil dieses Hosts (core, developer), eine eigene ragents.config.<profil>.ts an
|
|
11
|
+
Startet ein Profil dieses Hosts (core, developer, showcase), eine eigene ragents.config.<profil>.ts an
|
|
15
12
|
beliebiger Stelle oder einen Stand, den ragents connect schon geholt hat, ohne den Server zu
|
|
16
13
|
fragen. Ein eigenes Profil bekommt sein Web beim Start gebaut, ein geholter Stand bringt es aus
|
|
17
14
|
demselben Cache mit. Ohne Argument nennt der Befehl die Profile im Cache.`;
|
|
@@ -64,26 +61,6 @@ export const selectCachedProfile = (cached: readonly CachedProfile[], profile: s
|
|
|
64
61
|
+ "nimm ragents connect <server-url>.");
|
|
65
62
|
};
|
|
66
63
|
|
|
67
|
-
const PROFILE_FILE = /^ragents\.config\.([a-z0-9]+(?:-[a-z0-9]+)*)\.ts$/;
|
|
68
|
-
|
|
69
|
-
export interface LocalProfile {
|
|
70
|
-
readonly profile: string;
|
|
71
|
-
readonly profileFile: string;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Ein Profil ist ein Name neben dem Host (Checkout wie Paket) oder ein Pfad zu einer ragents.config.<profil>.ts an beliebiger Stelle. */
|
|
75
|
-
export const localProfile = (selection: string, root = hostRoot(), caller = callerDirectory()): LocalProfile | undefined => {
|
|
76
|
-
if (!selection.includes("/") && !selection.endsWith(".ts")) {
|
|
77
|
-
const file = path.join(root, profileFileName(selection));
|
|
78
|
-
return statSync(file, { throwIfNoEntry: false })?.isFile() ? { profile: selection, profileFile: file } : undefined;
|
|
79
|
-
}
|
|
80
|
-
const profileFile = path.resolve(caller, selection);
|
|
81
|
-
const match = PROFILE_FILE.exec(path.basename(profileFile));
|
|
82
|
-
if (!match) throw new Error(`Eine Profildatei heißt ragents.config.<profil>.ts, nicht ${path.basename(profileFile)}`);
|
|
83
|
-
if (!statSync(profileFile, { throwIfNoEntry: false })?.isFile()) throw new Error(`Die Profildatei fehlt: ${profileFile}`);
|
|
84
|
-
return { profile: match[1]!, profileFile };
|
|
85
|
-
};
|
|
86
|
-
|
|
87
64
|
const main = async (): Promise<void> => {
|
|
88
65
|
const arguments_ = parseArguments(process.argv.slice(2));
|
|
89
66
|
const own = localProfile(arguments_.selection);
|