@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 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`), your
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) throw new Error(`${location}.token verweist auf die nicht gesetzte oder leere Umgebungsvariable ${raw.name}`);
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",
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.com/SchlenkR/RAgents#readme",
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 core Sammelboard einrichten öffnen und den Leitfaden abschließen. Erwartung: Die Liste steht als Kachel auf der Fläche; ihr Besitzer hat keine eigene Kachel.",
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 core den Einstieg Gesprächsrunde einrichten ausführen und den Aufbau abwarten. Die vom Run gesetzte Aufteilung ansehen.",
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 core zwei kurze Unterhaltungen zu einer Leseliste und einem Wochenplan anlegen und ihre Antworten abwarten.",
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 core 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.",
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 core-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.",
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 core-Run den Koordinator bitten, im Run-Arbeitsverzeichnis die Datei reading-list.md mit drei neutralen Lesethemen anzulegen.",
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 core-Run Leseliste anlegen und darin notes.md mit dem Inhalt Lesen vorbereiten schreiben lassen.",
166
- "Einen zweiten core-Run Wochenplan anlegen und dort ebenfalls notes.md schreiben lassen, diesmal mit dem Inhalt Woche planen. Keine Datei zwischen Runs kopieren.",
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 core Sammelboard einrichten öffnen und den Leitfaden abschließen. Einen eindeutig erkennbaren Eintrag hinzufügen und die bestätigte Aktualisierung der Liste abwarten.",
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 core-Run drei Lernziele besprechen und im globalen Chat eine knappe Übersicht dieses Runs anfordern. Beide Antworten vollständig abwarten und die globale Modellwahl merken.",
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.",
@@ -21,7 +21,6 @@ export const config = {
21
21
  "ragents.watch",
22
22
  "ragents.transcript",
23
23
  "ragents.actor-programs",
24
- "ragents.reference",
25
24
  "ragents.lsp-roslyn",
26
25
  "ragents.lsp-fsharp",
27
26
  "ragents.lsp-typescript",
@@ -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, rmSync, statSync, writeFileSync } from "node:fs";
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 { profileFileName } from "../../apps/server/src/config-file.ts";
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 { readProfileTarget, type ProfileTarget } from "../../apps/server/src/profile-target.ts";
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
- Vorgabeprofil ist ${DEFAULT_PROFILE}. Exit-Code: 0 fertig, 2 abgebrochen, 1 fehlgeschlagen oder
35
- Verbindungsproblem. Die letzte Zeile auf stdout ist "run: <id>". Die Adresse kommt aus
36
- <Datenordner>/host.json, sonst aus RAGENTS_URL, sonst aus host.PORT des Profils; RAGENTS_TOKEN
37
- geht als Bearer-Token mit, falls das Profil eine Anmeldung verlangt. Der so gestartete Host baut
38
- die Oberfläche nicht - ein Agent braucht sie nicht; mit Oberfläche startet ragents start <profil>.`;
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") ?? DEFAULT_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 interface HostRecord {
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.select, { runId, optionId: WORKSPACE_BINDING_OPTION_ID, value: { kind: "path", path: folder } });
355
- note(`== Run ${runId} auf ${folder} (${baseUrl})`);
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 rpc.call(coreContracts.chat.send, { runId, text: command.text });
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
- rmSync(hostRecordFile(target.dataDirectory), { force: true });
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
- rmSync(hostRecordFile(target.dataDirectory), { force: true });
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.length === 0 || argv[0] === "--help") {
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: `${REPOSITORY_URL}#readme`,
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 = (command: { file: string; args: readonly string[]; cwd: string }, env: NodeJS.ProcessEnv): Promise<number> => new Promise((resolve, reject) => {
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 dataDirectory = started.dataDirectory ?? (await readProfileTarget(started.profile, started.profileFile)).dataDirectory;
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
- return run(hostScript("apps/server/src/main.ts", started.port !== undefined ? ["--port", String(started.port)] : []), { ...env, WEB_DIST_DIR: webDirectory });
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. */
@@ -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 { hostRoot } from "../../apps/server/src/host-version.ts";
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);