@gaia-ai/ui 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 keytec GmbH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # @gaia-ai/ui
2
+
3
+ GAIA home-rooted cockpit: the `gaia ui` command plugin (renderer resolved dynamically).
4
+
5
+ Part of the GAIA conductor. Install the meta package `@gaia-ai/gaia` to get the `gaia` CLI with all plugins. Source: https://git.key-tec.de/keytec/gaia (conductor/).
@@ -0,0 +1,2 @@
1
+ export { cmdUi, default, type RunGaiaUiOptions, type UiDeps } from './ui.js';
2
+ export { buildHomeConnection, type HomeConnection, resolveAgentCommand, resolveDefaultProject, resolveUiConfigPath, } from './ui-home.js';
@@ -0,0 +1,5 @@
1
+ // GAIA-201: `@gaia-ai/ui` — the home-rooted `gaia ui` command plugin. The
2
+ // default export is the `GaiaCommandPlugin` the host mounts; `cmdUi` + the
3
+ // home-connection builders are exported for tests.
4
+ export { cmdUi, default } from './ui.js';
5
+ export { buildHomeConnection, resolveAgentCommand, resolveDefaultProject, resolveUiConfigPath, } from './ui-home.js';
@@ -0,0 +1,2 @@
1
+ import type { GaiaPreset } from '@gaia-ai/core';
2
+ export declare const commands: GaiaPreset['commands'];
@@ -0,0 +1,8 @@
1
+ export const commands = (acc) => [
2
+ ...acc,
3
+ {
4
+ name: 'ui',
5
+ plugin: '@gaia-ai/ui',
6
+ describe: 'interactive terminal UI: project dashboard, ticket list, ticket detail',
7
+ },
8
+ ];
@@ -0,0 +1,65 @@
1
+ import { type ConductorRegistryEntry, type MachineContext } from '@gaia-ai/core';
2
+ /**
3
+ * The resolved home control-plane connection passed to the renderer launcher.
4
+ * A structural mirror of `@gaia-ai/plugin-gaia-ui`'s `HomeConnection` — kept
5
+ * local so this package needs NO compile-time dependency on the renderer (it
6
+ * resolves the renderer dynamically, ESLint-style, at runtime).
7
+ */
8
+ export interface HomeConnection {
9
+ baseUrl: string;
10
+ jsonapiPrefix?: string | undefined;
11
+ authPlugins: unknown[];
12
+ configPath?: string | undefined;
13
+ authProfile?: string | undefined;
14
+ }
15
+ /** The `session` oauth2 client-credentials descriptor, matching the connection
16
+ * config template shape exactly so home reads authenticate identically to a
17
+ * project-rooted `gaia dropsh`. */
18
+ export interface SessionAuthDescriptor {
19
+ plugin: '@dropsh/plugin-oauth2';
20
+ export: 'oauth2Plugin';
21
+ with: {
22
+ id: 'session';
23
+ default: true;
24
+ type: 'oauth2_client_credentials';
25
+ client_id: string;
26
+ client_secret: string;
27
+ token_url: string;
28
+ scope: 'gaia:session';
29
+ };
30
+ }
31
+ /**
32
+ * Build the home control-plane connection from the machine context. Throws an
33
+ * actionable error when the context is missing `base_url` / `client_id` /
34
+ * `client_secret` (there is no project config to fall back to). The returned
35
+ * `authPlugins` carries a single `session` oauth2 descriptor; `configPath` is
36
+ * filled in by the entry once it has resolved the connection config file.
37
+ */
38
+ export declare function buildHomeConnection(machine: Partial<MachineContext>): HomeConnection;
39
+ /**
40
+ * Resolve the default project (opaque group) from the conductor registry: the
41
+ * `preferred` project when it is registered, else the first entry's project.
42
+ * `undefined` when the registry is empty — a valid empty cockpit.
43
+ */
44
+ export declare function resolveDefaultProject(entries: ConductorRegistryEntry[], preferred?: string): string | undefined;
45
+ /**
46
+ * Resolve the agent-launch command builder from the home config. Turns a TUI
47
+ * prompt into the shell command herdr runs. Defaults to `claude` (an empty
48
+ * prompt yields the bare launch command). A machine context may name a
49
+ * different `agent_command` binary.
50
+ */
51
+ export declare function resolveAgentCommand(machine: Partial<MachineContext> & {
52
+ agent_command?: string;
53
+ }): (prompt: string) => string;
54
+ /**
55
+ * Resolve which connection config `gaia ui` reads. A user-authored home
56
+ * `~/.gaia/gaia.config.js` (`userConfigPath`) wins — a real home-level cockpit
57
+ * connection — used verbatim (`fallback: false`). Otherwise the shipped
58
+ * machine-context fallback (`fallbackPath`) is used (`fallback: true`), which
59
+ * requires a complete machine context. Pure: the caller does the
60
+ * `$DROPSH_CONFIG` / `$GAIA_MACHINE_CONTEXT` wiring.
61
+ */
62
+ export declare function resolveUiConfigPath(userConfigPath: string, fallbackPath: string): {
63
+ path: string;
64
+ fallback: boolean;
65
+ };
@@ -0,0 +1,82 @@
1
+ // GAIA-201 (was GAIA-194): home-rooted bootstrap builders for `gaia ui`. Pure
2
+ // functions that turn the user-global machine context (identity + connection) +
3
+ // the conductor registry into everything `runGaiaUi` needs — WITHOUT a project
4
+ // engine `conductor.config.js`. The `gaia ui` entry (`ui.ts`) wires them.
5
+ import { existsSync } from 'node:fs';
6
+ import { shellQuote, } from '@gaia-ai/core';
7
+ /**
8
+ * Build the home control-plane connection from the machine context. Throws an
9
+ * actionable error when the context is missing `base_url` / `client_id` /
10
+ * `client_secret` (there is no project config to fall back to). The returned
11
+ * `authPlugins` carries a single `session` oauth2 descriptor; `configPath` is
12
+ * filled in by the entry once it has resolved the connection config file.
13
+ */
14
+ export function buildHomeConnection(machine) {
15
+ const baseUrl = (machine.base_url ?? '').trim();
16
+ const clientId = (machine.client_id ?? '').trim();
17
+ const clientSecret = (machine.client_secret ?? '').trim();
18
+ const missing = [];
19
+ if (baseUrl === '')
20
+ missing.push('base_url');
21
+ if (clientId === '')
22
+ missing.push('client_id');
23
+ if (clientSecret === '')
24
+ missing.push('client_secret');
25
+ if (missing.length > 0) {
26
+ throw new Error(`gaia ui: the machine context is missing ${missing.join(', ')} — ` +
27
+ `run 'gaia conductor init --base-url <url>' to onboard this machine.`);
28
+ }
29
+ const descriptor = {
30
+ plugin: '@dropsh/plugin-oauth2',
31
+ export: 'oauth2Plugin',
32
+ with: {
33
+ id: 'session',
34
+ default: true,
35
+ type: 'oauth2_client_credentials',
36
+ client_id: clientId,
37
+ client_secret: clientSecret,
38
+ token_url: `${baseUrl}/oauth/token`,
39
+ scope: 'gaia:session',
40
+ },
41
+ };
42
+ return {
43
+ baseUrl,
44
+ jsonapiPrefix: '/jsonapi',
45
+ authPlugins: [descriptor],
46
+ authProfile: 'session',
47
+ };
48
+ }
49
+ /**
50
+ * Resolve the default project (opaque group) from the conductor registry: the
51
+ * `preferred` project when it is registered, else the first entry's project.
52
+ * `undefined` when the registry is empty — a valid empty cockpit.
53
+ */
54
+ export function resolveDefaultProject(entries, preferred) {
55
+ if (preferred && entries.some((e) => e.project === preferred)) {
56
+ return preferred;
57
+ }
58
+ return entries[0]?.project;
59
+ }
60
+ /**
61
+ * Resolve the agent-launch command builder from the home config. Turns a TUI
62
+ * prompt into the shell command herdr runs. Defaults to `claude` (an empty
63
+ * prompt yields the bare launch command). A machine context may name a
64
+ * different `agent_command` binary.
65
+ */
66
+ export function resolveAgentCommand(machine) {
67
+ const bin = (machine.agent_command ?? '').trim() || 'claude';
68
+ return (prompt) => prompt.trim() === '' ? bin : `${bin} ${shellQuote(prompt)}`;
69
+ }
70
+ /**
71
+ * Resolve which connection config `gaia ui` reads. A user-authored home
72
+ * `~/.gaia/gaia.config.js` (`userConfigPath`) wins — a real home-level cockpit
73
+ * connection — used verbatim (`fallback: false`). Otherwise the shipped
74
+ * machine-context fallback (`fallbackPath`) is used (`fallback: true`), which
75
+ * requires a complete machine context. Pure: the caller does the
76
+ * `$DROPSH_CONFIG` / `$GAIA_MACHINE_CONTEXT` wiring.
77
+ */
78
+ export function resolveUiConfigPath(userConfigPath, fallbackPath) {
79
+ return existsSync(userConfigPath)
80
+ ? { path: userConfigPath, fallback: false }
81
+ : { path: fallbackPath, fallback: true };
82
+ }
@@ -0,0 +1,73 @@
1
+ import { type ConductorRegistryEntry, type GaiaCommandHost, type GaiaCommandPlugin, type MachineContext } from '@gaia-ai/core';
2
+ import { type AgentLaunchHost } from '@gaia-ai/plugin-herdr';
3
+ import { type HomeConnection } from './ui-home.js';
4
+ /** Prompt-level agent service the renderer consumes (mirror of gaia-ui's `TuiAgentService`). */
5
+ interface TuiAgentService {
6
+ launch(o: {
7
+ group: string;
8
+ cwd: string;
9
+ prompt: string;
10
+ env: Record<string, string | undefined>;
11
+ isNew?: boolean | undefined;
12
+ label?: string | undefined;
13
+ }): Promise<{
14
+ id: string;
15
+ group: string;
16
+ label: string;
17
+ }>;
18
+ list(group: string): Promise<{
19
+ id: string;
20
+ group: string;
21
+ label: string;
22
+ }[]>;
23
+ focus(id: string): Promise<void>;
24
+ hide(group: string): Promise<void>;
25
+ kill(id: string): Promise<void>;
26
+ }
27
+ /** A dropsh program (only the `parseAsync` the launcher drives). */
28
+ interface DropshProgram {
29
+ parseAsync(argv: string[], opts: {
30
+ from: 'user';
31
+ }): Promise<unknown>;
32
+ }
33
+ /** Options for gaia-ui's `runGaiaUi` (mirror of its `GaiaUiLauncherOptions`). */
34
+ export interface RunGaiaUiOptions {
35
+ connection: HomeConnection;
36
+ cwd: string;
37
+ project?: string | undefined;
38
+ agents?: TuiAgentService | undefined;
39
+ currentUser?: string | undefined;
40
+ ownConductorMachineId?: string | undefined;
41
+ createPrompt?: string | undefined;
42
+ buildProgram: (opts: {
43
+ plugins: unknown[];
44
+ }) => DropshProgram;
45
+ renderer?: ((opts: unknown) => unknown) | undefined;
46
+ }
47
+ /** Test/composition seams for `gaia ui`. */
48
+ export interface UiDeps {
49
+ /** Machine context (identity + connection); default: read from disk. */
50
+ machine?: Partial<MachineContext>;
51
+ /** Conductor registry entries; default: `listRegisteredConductors()`. */
52
+ registryEntries?: ConductorRegistryEntry[];
53
+ /** herdr-backed agent host; default: `herdrAgentHost(exec herdr)`. */
54
+ agentHost?: AgentLaunchHost;
55
+ /** The renderer launcher; default: the dynamically-resolved `runGaiaUi`. */
56
+ runUi?: (opts: RunGaiaUiOptions) => Promise<void>;
57
+ /** Pre-resolved home connection config path; default: resolved from ~/.gaia or the shipped fallback. */
58
+ homeConfigPath?: string;
59
+ /** Host contract (resolveBases) for renderer resolution; default: cwd + this install. */
60
+ host?: GaiaCommandHost;
61
+ }
62
+ /**
63
+ * `gaia ui` — launch the home-rooted interactive terminal UI. Reads the
64
+ * user-global machine context + the conductor registry (NOT a project engine
65
+ * config), resolves the connection from the home `gaia.config.js` scope, adapts
66
+ * a herdr-backed agent host to the renderer's prompt-level service, and
67
+ * delegates to `@gaia-ai/plugin-gaia-ui`'s `runGaiaUi`. `$GAIA_UI_PLUGIN`
68
+ * overrides the renderer module (dev).
69
+ */
70
+ export declare function cmdUi(deps?: UiDeps): Promise<void>;
71
+ /** The `ui` command plugin the host mounts (`gaia ui`). */
72
+ declare const uiCommandPlugin: GaiaCommandPlugin;
73
+ export default uiCommandPlugin;
package/dist/src/ui.js ADDED
@@ -0,0 +1,152 @@
1
+ import { createRequire } from 'node:module';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ import { CommandRunner, createLogger, exec, homeFallbackGaiaConfigPath, listRegisteredConductors, machineContextPath, readMachineContext, setDefaultCommandRunner, } from '@gaia-ai/core';
6
+ import { herdrAgentHost } from '@gaia-ai/plugin-herdr';
7
+ import { buildProgram as buildDropshProgram } from 'dropsh';
8
+ import { buildHomeConnection, resolveAgentCommand, resolveDefaultProject, resolveUiConfigPath, } from './ui-home.js';
9
+ function loggerFor(checkoutRoot) {
10
+ const logger = createLogger({ checkoutRoot });
11
+ setDefaultCommandRunner(new CommandRunner(logger));
12
+ return logger;
13
+ }
14
+ /**
15
+ * Resolve + dynamically import `@gaia-ai/plugin-gaia-ui` (the renderer launcher),
16
+ * ESLint-style over the host bases. This package never statically imports the
17
+ * renderer — it is a swappable plugin. Returns undefined (and logs) when the
18
+ * package is not installed.
19
+ */
20
+ async function loadGaiaUiModule(logger, bases) {
21
+ let resolved;
22
+ for (const base of bases) {
23
+ try {
24
+ resolved = createRequire(base).resolve('@gaia-ai/plugin-gaia-ui');
25
+ break;
26
+ }
27
+ catch {
28
+ // try the next base
29
+ }
30
+ }
31
+ if (resolved === undefined) {
32
+ logger.error({}, 'gaia ui: could not resolve @gaia-ai/plugin-gaia-ui. ' +
33
+ 'Install it, or set $GAIA_UI_PLUGIN to a renderer plugin path.');
34
+ return undefined;
35
+ }
36
+ return (await import(pathToFileURL(resolved).href));
37
+ }
38
+ /**
39
+ * `gaia ui` — launch the home-rooted interactive terminal UI. Reads the
40
+ * user-global machine context + the conductor registry (NOT a project engine
41
+ * config), resolves the connection from the home `gaia.config.js` scope, adapts
42
+ * a herdr-backed agent host to the renderer's prompt-level service, and
43
+ * delegates to `@gaia-ai/plugin-gaia-ui`'s `runGaiaUi`. `$GAIA_UI_PLUGIN`
44
+ * overrides the renderer module (dev).
45
+ */
46
+ export async function cmdUi(deps = {}) {
47
+ const logger = loggerFor(homedir());
48
+ const host = deps.host ?? {
49
+ resolveBases: [`${process.cwd()}/`, import.meta.url],
50
+ };
51
+ const mcPath = machineContextPath();
52
+ const machine = deps.machine ?? (await readMachineContext(mcPath).catch(() => ({})));
53
+ // Which connection config `gaia ui` reads: a user-authored
54
+ // `~/.gaia/gaia.config.js` wins; otherwise the shipped machine-context
55
+ // fallback. The test seam `homeConfigPath` forces a path (fallback branch).
56
+ const userHomeConfig = join(homedir(), '.gaia', 'gaia.config.js');
57
+ const resolved = deps.homeConfigPath
58
+ ? { path: deps.homeConfigPath, fallback: true }
59
+ : resolveUiConfigPath(userHomeConfig, homeFallbackGaiaConfigPath());
60
+ let connection;
61
+ if (resolved.fallback) {
62
+ try {
63
+ connection = buildHomeConnection(machine);
64
+ }
65
+ catch (err) {
66
+ logger.error({}, err instanceof Error ? err.message : String(err));
67
+ process.exitCode = 1;
68
+ return;
69
+ }
70
+ if (!process.env.GAIA_MACHINE_CONTEXT) {
71
+ process.env.GAIA_MACHINE_CONTEXT = mcPath;
72
+ }
73
+ }
74
+ else {
75
+ connection = { baseUrl: '', authProfile: 'session', authPlugins: [] };
76
+ }
77
+ connection.configPath = resolved.path;
78
+ const entries = deps.registryEntries ?? (await listRegisteredConductors());
79
+ const project = resolveDefaultProject(entries);
80
+ const projectEntry = entries.find((e) => e.project === project);
81
+ const cwd = projectEntry?.path ?? homedir();
82
+ const ownConductorMachineId = projectEntry?.id;
83
+ const agentCommand = resolveAgentCommand(machine);
84
+ const agentHost = deps.agentHost ?? herdrAgentHost((args) => exec('herdr', args));
85
+ const agents = {
86
+ launch: (o) => agentHost.launch({
87
+ group: o.group,
88
+ cwd: o.cwd,
89
+ command: agentCommand(o.prompt),
90
+ env: o.env,
91
+ ...(o.label !== undefined ? { label: o.label } : {}),
92
+ }),
93
+ list: (group) => agentHost.list(group),
94
+ focus: (id) => agentHost.focus(id),
95
+ hide: (group) => agentHost.hide(group),
96
+ kill: (id) => agentHost.kill(id),
97
+ };
98
+ let renderer;
99
+ const override = process.env.GAIA_UI_PLUGIN;
100
+ if (override) {
101
+ try {
102
+ const mod = (await import(pathToFileURL(override).href));
103
+ const factory = mod.default ?? mod.ticketRendererPlugin;
104
+ if (typeof factory !== 'function') {
105
+ throw new Error('module exports no renderer-plugin factory');
106
+ }
107
+ renderer = factory;
108
+ }
109
+ catch (err) {
110
+ logger.error({ override }, `gaia ui: could not load $GAIA_UI_PLUGIN at ${override} ` +
111
+ `(${err instanceof Error ? err.message : String(err)}).`);
112
+ process.exitCode = 1;
113
+ return;
114
+ }
115
+ }
116
+ let run = deps.runUi;
117
+ if (!run) {
118
+ const uiMod = await loadGaiaUiModule(logger, host.resolveBases);
119
+ if (uiMod === undefined) {
120
+ process.exitCode = 1;
121
+ return;
122
+ }
123
+ run = uiMod.runGaiaUi;
124
+ }
125
+ await run({
126
+ connection,
127
+ cwd,
128
+ ...(project !== undefined ? { project } : {}),
129
+ agents,
130
+ ...(machine.user_id ? { currentUser: machine.user_id } : {}),
131
+ ...(ownConductorMachineId ? { ownConductorMachineId } : {}),
132
+ buildProgram: (o) => buildDropshProgram({
133
+ plugins: o.plugins,
134
+ }),
135
+ ...(renderer ? { renderer } : {}),
136
+ });
137
+ }
138
+ /** The `ui` command plugin the host mounts (`gaia ui`). */
139
+ const uiCommandPlugin = {
140
+ kind: 'command',
141
+ name: 'ui',
142
+ describe: 'interactive terminal UI: project dashboard, ticket list, ticket detail',
143
+ register(program, host) {
144
+ program
145
+ .command('ui')
146
+ .description('interactive terminal UI: project dashboard, ticket list, ticket detail')
147
+ .action(async () => {
148
+ await cmdUi({ host });
149
+ });
150
+ },
151
+ };
152
+ export default uiCommandPlugin;
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@gaia-ai/ui",
3
+ "version": "0.6.0",
4
+ "description": "GAIA home-rooted cockpit: the `gaia ui` command plugin (renderer resolved dynamically).",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "engines": {
8
+ "node": ">=20.19"
9
+ },
10
+ "exports": {
11
+ ".": "./dist/src/index.js",
12
+ "./preset": "./dist/src/preset.js",
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "dist/src",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://git.key-tec.de/keytec/gaia.git",
26
+ "directory": "conductor/ui"
27
+ },
28
+ "dependencies": {
29
+ "@gaia-ai/core": "^0.6.0",
30
+ "@gaia-ai/plugin-herdr": "^0.6.0",
31
+ "commander": "^12.1.0",
32
+ "dropsh": "^0.5.8"
33
+ }
34
+ }