gestalt-mobile 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +133 -0
- package/dist/client/assets/index-0wjiXBGW.js +10 -0
- package/dist/client/assets/index-CfoipqOl.css +1 -0
- package/dist/client/branding/dyne-logotype-black.svg +17 -0
- package/dist/client/branding/dyne-logotype-white.svg +17 -0
- package/dist/client/branding/p_glogo_grey.svg +8 -0
- package/dist/client/branding/p_glogo_white.svg +8 -0
- package/dist/client/branding/t_glogo_grey.svg +9 -0
- package/dist/client/branding/t_glogo_white.svg +9 -0
- package/dist/client/index.html +19 -0
- package/dist/server/server/app.js +148 -0
- package/dist/server/server/cli/list-sessions.js +13 -0
- package/dist/server/server/cli.js +126 -0
- package/dist/server/server/composition.js +237 -0
- package/dist/server/server/config.js +37 -0
- package/dist/server/server/features/catalog/application/ports.js +6 -0
- package/dist/server/server/features/catalog/get-bootstrap/endpoint.js +9 -0
- package/dist/server/server/features/catalog/get-bootstrap/request.js +6 -0
- package/dist/server/server/features/catalog/get-bootstrap/response.js +6 -0
- package/dist/server/server/features/catalog/get-bootstrap/use-case.js +19 -0
- package/dist/server/server/features/git/checkout-branch/endpoint.js +27 -0
- package/dist/server/server/features/git/clone-repository/endpoint.js +19 -0
- package/dist/server/server/features/git/get-summary/endpoint.js +13 -0
- package/dist/server/server/features/git/pull-rebase/endpoint.js +22 -0
- package/dist/server/server/features/git/push-policy.js +14 -0
- package/dist/server/server/features/git/push-upstream/endpoint.js +29 -0
- package/dist/server/server/features/git/refresh/endpoint.js +24 -0
- package/dist/server/server/features/git/refresh-policy.js +8 -0
- package/dist/server/server/features/health/get-health/endpoint.js +8 -0
- package/dist/server/server/features/health/get-health/request.js +6 -0
- package/dist/server/server/features/health/get-health/response.js +6 -0
- package/dist/server/server/features/sessions/application/ports.js +6 -0
- package/dist/server/server/features/sessions/application/resume-command.js +22 -0
- package/dist/server/server/features/sessions/application/start-settings.js +6 -0
- package/dist/server/server/features/sessions/forget-session/endpoint.js +15 -0
- package/dist/server/server/features/sessions/get-history/endpoint.js +20 -0
- package/dist/server/server/features/sessions/get-history/history-mapper.js +82 -0
- package/dist/server/server/features/sessions/get-session/endpoint.js +17 -0
- package/dist/server/server/features/sessions/history/use-case.js +8 -0
- package/dist/server/server/features/sessions/interaction/kind.js +8 -0
- package/dist/server/server/features/sessions/interaction/response-guard.js +8 -0
- package/dist/server/server/features/sessions/interaction/response-validator.js +35 -0
- package/dist/server/server/features/sessions/interrupt-turn/endpoint.js +19 -0
- package/dist/server/server/features/sessions/interrupt-turn/use-case.js +10 -0
- package/dist/server/server/features/sessions/lifecycle/use-case.js +15 -0
- package/dist/server/server/features/sessions/list-recent-threads/endpoint.js +11 -0
- package/dist/server/server/features/sessions/list-sessions/endpoint.js +12 -0
- package/dist/server/server/features/sessions/list-sessions/use-case.js +8 -0
- package/dist/server/server/features/sessions/model/errors.js +13 -0
- package/dist/server/server/features/sessions/model/events.js +6 -0
- package/dist/server/server/features/sessions/model/relay-session.js +122 -0
- package/dist/server/server/features/sessions/model/value-objects.js +22 -0
- package/dist/server/server/features/sessions/promote-recent-thread/endpoint.js +29 -0
- package/dist/server/server/features/sessions/promote-recent-thread/use-case.js +26 -0
- package/dist/server/server/features/sessions/release-session/endpoint.js +25 -0
- package/dist/server/server/features/sessions/release-session/use-case.js +9 -0
- package/dist/server/server/features/sessions/respond-interaction/endpoint.js +24 -0
- package/dist/server/server/features/sessions/restore-session/endpoint.js +27 -0
- package/dist/server/server/features/sessions/restore-session/use-case.js +17 -0
- package/dist/server/server/features/sessions/session-events/endpoint.js +44 -0
- package/dist/server/server/features/sessions/start-session/endpoint.js +37 -0
- package/dist/server/server/features/sessions/start-session/request.js +22 -0
- package/dist/server/server/features/sessions/start-session/use-case.js +29 -0
- package/dist/server/server/features/sessions/start-turn/endpoint.js +20 -0
- package/dist/server/server/features/sessions/stop-session/endpoint.js +25 -0
- package/dist/server/server/features/sessions/stop-session/use-case.js +9 -0
- package/dist/server/server/features/sessions/turn/start-policy.js +8 -0
- package/dist/server/server/index.js +7 -0
- package/dist/server/server/main.js +14 -0
- package/dist/server/server/platform/catalog/codex-profile-catalog.js +42 -0
- package/dist/server/server/platform/catalog/filesystem-workspace-catalog.js +60 -0
- package/dist/server/server/platform/catalog/profile-command.js +19 -0
- package/dist/server/server/platform/catalog/workspace-id.js +9 -0
- package/dist/server/server/platform/codex/approval-policy.js +8 -0
- package/dist/server/server/platform/codex/codex-process-launcher.js +27 -0
- package/dist/server/server/platform/codex/error-code.js +10 -0
- package/dist/server/server/platform/codex/json-rpc-client.js +71 -0
- package/dist/server/server/platform/codex/normalizer.js +81 -0
- package/dist/server/server/platform/codex/protocol-compatibility.js +12 -0
- package/dist/server/server/platform/codex/recent-thread-lister.js +49 -0
- package/dist/server/server/platform/codex/server-request.js +14 -0
- package/dist/server/server/platform/codex/session-runtime.js +142 -0
- package/dist/server/server/platform/codex/thread-id.js +10 -0
- package/dist/server/server/platform/events/replay.js +12 -0
- package/dist/server/server/platform/events/session-event-bus.js +17 -0
- package/dist/server/server/platform/events/slow-client.js +9 -0
- package/dist/server/server/platform/events/websocket-heartbeat.js +20 -0
- package/dist/server/server/platform/git/command.js +13 -0
- package/dist/server/server/platform/git/git-fetch-coordinator.js +40 -0
- package/dist/server/server/platform/git/git-inspector.js +116 -0
- package/dist/server/server/platform/git/git-summary-cache.js +26 -0
- package/dist/server/server/platform/git/git-summary.js +8 -0
- package/dist/server/server/platform/http/idempotency.js +9 -0
- package/dist/server/server/platform/http/problem-handler.js +32 -0
- package/dist/server/server/platform/http/problem.js +15 -0
- package/dist/server/server/platform/observability/safe-log.js +9 -0
- package/dist/server/server/platform/persistence/migrate.js +9 -0
- package/dist/server/server/platform/persistence/retention.js +9 -0
- package/dist/server/server/platform/persistence/sqlite-event-journal.js +50 -0
- package/dist/server/server/platform/persistence/sqlite-idempotency-store.js +22 -0
- package/dist/server/server/platform/persistence/sqlite-pending-interaction-store.js +33 -0
- package/dist/server/server/platform/persistence/sqlite-session-repository.js +43 -0
- package/dist/server/server/platform/persistence/sqlite.js +14 -0
- package/dist/server/server/platform/persistence/state-path.js +16 -0
- package/dist/server/server/platform/runtime/clock.js +9 -0
- package/dist/server/server/platform/runtime/concurrency.js +15 -0
- package/dist/server/server/platform/runtime/retry-policy.js +9 -0
- package/dist/server/server/platform/runtime/session-supervisor.js +41 -0
- package/dist/server/shared/contracts/problem.js +6 -0
- package/dist/server/shared/contracts/session-event.js +6 -0
- package/package.json +78 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
import fastifyStatic from '@fastify/static';
|
|
7
|
+
import fastify, {} from 'fastify';
|
|
8
|
+
import { registerGetHealth } from './features/health/get-health/endpoint.js';
|
|
9
|
+
import { registerGetBootstrap } from './features/catalog/get-bootstrap/endpoint.js';
|
|
10
|
+
import { registerGetGitSummary } from './features/git/get-summary/endpoint.js';
|
|
11
|
+
import { registerPushUpstream } from './features/git/push-upstream/endpoint.js';
|
|
12
|
+
import { registerRefreshGit } from './features/git/refresh/endpoint.js';
|
|
13
|
+
import { registerPullRebase } from './features/git/pull-rebase/endpoint.js';
|
|
14
|
+
import { registerCheckoutBranch } from './features/git/checkout-branch/endpoint.js';
|
|
15
|
+
import { registerCloneRepository } from './features/git/clone-repository/endpoint.js';
|
|
16
|
+
import { registerGetSession } from './features/sessions/get-session/endpoint.js';
|
|
17
|
+
import { registerListSessions } from './features/sessions/list-sessions/endpoint.js';
|
|
18
|
+
import { registerListRecentThreads, } from './features/sessions/list-recent-threads/endpoint.js';
|
|
19
|
+
import { registerPromoteRecentThread } from './features/sessions/promote-recent-thread/endpoint.js';
|
|
20
|
+
import { registerGetHistory } from './features/sessions/get-history/endpoint.js';
|
|
21
|
+
import { registerStartSession } from './features/sessions/start-session/endpoint.js';
|
|
22
|
+
import { registerStartTurn } from './features/sessions/start-turn/endpoint.js';
|
|
23
|
+
import { registerInterruptTurn } from './features/sessions/interrupt-turn/endpoint.js';
|
|
24
|
+
import { registerStopSession } from './features/sessions/stop-session/endpoint.js';
|
|
25
|
+
import { registerRestoreSession } from './features/sessions/restore-session/endpoint.js';
|
|
26
|
+
import { registerReleaseSession } from './features/sessions/release-session/endpoint.js';
|
|
27
|
+
import { registerForgetSession } from './features/sessions/forget-session/endpoint.js';
|
|
28
|
+
import { registerRespondInteraction } from './features/sessions/respond-interaction/endpoint.js';
|
|
29
|
+
import { stopSession } from './features/sessions/lifecycle/use-case.js';
|
|
30
|
+
import { registerSessionEvents } from './features/sessions/session-events/endpoint.js';
|
|
31
|
+
import { registerProblemHandler } from './platform/http/problem-handler.js';
|
|
32
|
+
export async function buildApp(deps) {
|
|
33
|
+
const app = fastify({ logger: false });
|
|
34
|
+
if (deps.staticDir)
|
|
35
|
+
await app.register(fastifyStatic, { root: deps.staticDir });
|
|
36
|
+
registerGetHealth(app, deps.health);
|
|
37
|
+
if (deps.bootstrap)
|
|
38
|
+
registerGetBootstrap(app, deps.bootstrap);
|
|
39
|
+
if (deps.recentThreads)
|
|
40
|
+
registerListRecentThreads(app, deps.recentThreads);
|
|
41
|
+
if (deps.sessionRoutes) {
|
|
42
|
+
registerStartSession(app, {
|
|
43
|
+
...deps.sessionRoutes,
|
|
44
|
+
reportFailure: (operation, error) => deps.logger.error(`${operation} failed: ${safeErrorLabel(error)}`),
|
|
45
|
+
});
|
|
46
|
+
registerGetSession(app, deps.sessionRoutes.find);
|
|
47
|
+
if (deps.sessionRoutes.list)
|
|
48
|
+
registerListSessions(app, { list: deps.sessionRoutes.list });
|
|
49
|
+
if (deps.sessionRoutes.readHistory)
|
|
50
|
+
registerGetHistory(app, {
|
|
51
|
+
find: deps.sessionRoutes.find,
|
|
52
|
+
read: deps.sessionRoutes.readHistory,
|
|
53
|
+
currentSequence: deps.sessionRoutes.currentSequence ?? (() => 0),
|
|
54
|
+
});
|
|
55
|
+
if (deps.sessionRoutes.startTurn)
|
|
56
|
+
registerStartTurn(app, {
|
|
57
|
+
find: deps.sessionRoutes.find,
|
|
58
|
+
start: deps.sessionRoutes.startTurn,
|
|
59
|
+
save: deps.sessionRoutes.save,
|
|
60
|
+
});
|
|
61
|
+
if (deps.sessionRoutes.interruptTurn)
|
|
62
|
+
registerInterruptTurn(app, {
|
|
63
|
+
find: deps.sessionRoutes.find,
|
|
64
|
+
interrupt: deps.sessionRoutes.interruptTurn,
|
|
65
|
+
});
|
|
66
|
+
if (deps.sessionRoutes.restore)
|
|
67
|
+
registerRestoreSession(app, {
|
|
68
|
+
find: deps.sessionRoutes.find,
|
|
69
|
+
restore: deps.sessionRoutes.restore,
|
|
70
|
+
save: deps.sessionRoutes.save,
|
|
71
|
+
idempotency: deps.sessionRoutes.idempotency,
|
|
72
|
+
});
|
|
73
|
+
if (deps.recentThreads && deps.sessionRoutes.promoteRecent)
|
|
74
|
+
registerPromoteRecentThread(app, {
|
|
75
|
+
list: () => deps.recentThreads.list(),
|
|
76
|
+
promote: deps.sessionRoutes.promoteRecent,
|
|
77
|
+
});
|
|
78
|
+
if (deps.sessionRoutes.release && deps.sessionRoutes.close)
|
|
79
|
+
registerReleaseSession(app, {
|
|
80
|
+
find: deps.sessionRoutes.find,
|
|
81
|
+
release: deps.sessionRoutes.release,
|
|
82
|
+
save: deps.sessionRoutes.save,
|
|
83
|
+
close: deps.sessionRoutes.close,
|
|
84
|
+
idempotency: deps.sessionRoutes.idempotency,
|
|
85
|
+
});
|
|
86
|
+
if (deps.sessionRoutes.remove && deps.sessionRoutes.close)
|
|
87
|
+
registerForgetSession(app, {
|
|
88
|
+
find: deps.sessionRoutes.find,
|
|
89
|
+
close: deps.sessionRoutes.close,
|
|
90
|
+
remove: deps.sessionRoutes.remove,
|
|
91
|
+
});
|
|
92
|
+
if (deps.sessionRoutes.close)
|
|
93
|
+
registerStopSession(app, {
|
|
94
|
+
find: deps.sessionRoutes.find,
|
|
95
|
+
stop: (session) => stopSession(session, deps.sessionRoutes.now()),
|
|
96
|
+
save: deps.sessionRoutes.save,
|
|
97
|
+
close: deps.sessionRoutes.close,
|
|
98
|
+
idempotency: deps.sessionRoutes.idempotency,
|
|
99
|
+
});
|
|
100
|
+
if (deps.sessionRoutes.replyInteraction && deps.interactions)
|
|
101
|
+
registerRespondInteraction(app, {
|
|
102
|
+
exists: (id) => deps.sessionRoutes.find(id) !== null,
|
|
103
|
+
resolve: (sessionId, requestId, resolvedAt) => deps.interactions.resolve(sessionId, requestId, resolvedAt),
|
|
104
|
+
validate: deps.interactions.validate,
|
|
105
|
+
reply: deps.sessionRoutes.replyInteraction,
|
|
106
|
+
resolved: deps.sessionRoutes.interactionResolved,
|
|
107
|
+
now: deps.sessionRoutes.now,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
if (deps.sessionEvents)
|
|
111
|
+
registerSessionEvents(app, deps.sessionEvents);
|
|
112
|
+
if (deps.gitSummary && deps.sessionRoutes)
|
|
113
|
+
registerGetGitSummary(app, { find: deps.sessionRoutes.find, inspect: deps.gitSummary.inspect });
|
|
114
|
+
if (deps.gitSummary && deps.sessionRoutes)
|
|
115
|
+
registerPushUpstream(app, {
|
|
116
|
+
find: deps.sessionRoutes.find,
|
|
117
|
+
inspect: deps.gitSummary.inspectForPush ?? deps.gitSummary.inspect,
|
|
118
|
+
push: deps.gitSummary.push,
|
|
119
|
+
idempotency: deps.sessionRoutes.idempotency,
|
|
120
|
+
});
|
|
121
|
+
if (deps.gitSummary && deps.sessionRoutes)
|
|
122
|
+
registerRefreshGit(app, {
|
|
123
|
+
find: deps.sessionRoutes.find,
|
|
124
|
+
refresh: deps.gitSummary.refresh,
|
|
125
|
+
idempotency: deps.sessionRoutes.idempotency,
|
|
126
|
+
});
|
|
127
|
+
if (deps.gitSummary?.pull && deps.gitSummary.checkout && deps.sessionRoutes) {
|
|
128
|
+
registerPullRebase(app, { find: deps.sessionRoutes.find, pull: deps.gitSummary.pull });
|
|
129
|
+
registerCheckoutBranch(app, {
|
|
130
|
+
find: deps.sessionRoutes.find,
|
|
131
|
+
checkout: deps.gitSummary.checkout,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
if (deps.gitSummary?.clone)
|
|
135
|
+
registerCloneRepository(app, { clone: deps.gitSummary.clone });
|
|
136
|
+
registerProblemHandler(app, Boolean(deps.staticDir));
|
|
137
|
+
return app;
|
|
138
|
+
}
|
|
139
|
+
function safeErrorLabel(error) {
|
|
140
|
+
if (typeof error === 'object' && error !== null && 'code' in error) {
|
|
141
|
+
const code = error.code;
|
|
142
|
+
if (typeof code === 'string' && /^[A-Z0-9_]+$/.test(code))
|
|
143
|
+
return code;
|
|
144
|
+
}
|
|
145
|
+
if (error instanceof Error && /^[A-Z0-9_]+$/.test(error.message))
|
|
146
|
+
return error.message;
|
|
147
|
+
return error instanceof Error ? error.name : 'UNKNOWN_ERROR';
|
|
148
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
import { buildResumeCommand } from '../features/sessions/application/resume-command.js';
|
|
7
|
+
export function formatSessions(sessions) {
|
|
8
|
+
if (!sessions.length)
|
|
9
|
+
return 'No sessions\n';
|
|
10
|
+
return sessions
|
|
11
|
+
.map((session) => `${session.id}\t${session.state}\t${session.profile}\t${session.workspaceId}\t${session.threadId ?? '-'}\n${session.threadId ? buildResumeCommand(session) : ''}`)
|
|
12
|
+
.join('\n');
|
|
13
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
import { execFile } from 'node:child_process';
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
|
+
import { readFile } from 'node:fs/promises';
|
|
9
|
+
import { dirname, resolve } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { promisify } from 'node:util';
|
|
12
|
+
import { composeRelayApp } from './composition.js';
|
|
13
|
+
import { CliUsageError, parseConfig } from './config.js';
|
|
14
|
+
import { CodexProfileCatalog } from './platform/catalog/codex-profile-catalog.js';
|
|
15
|
+
const runFile = promisify(execFile);
|
|
16
|
+
export const usage = `Usage: gestalt-mobile [options]
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--cwd <path> Workspace root (default: current directory)
|
|
20
|
+
--host <address> Listen address (default: 127.0.0.1)
|
|
21
|
+
--port <number> Listen port (default: 3000)
|
|
22
|
+
--data-dir <path> Directory for persistent application data
|
|
23
|
+
--help Show this help
|
|
24
|
+
--version Show the installed version`;
|
|
25
|
+
export function packageRoot(moduleUrl) {
|
|
26
|
+
let directory = dirname(fileURLToPath(moduleUrl));
|
|
27
|
+
while (true) {
|
|
28
|
+
if (existsSync(resolve(directory, 'package.json')))
|
|
29
|
+
return directory;
|
|
30
|
+
const parent = dirname(directory);
|
|
31
|
+
if (parent === directory)
|
|
32
|
+
throw new Error('Gestalt Mobile package root is unavailable');
|
|
33
|
+
directory = parent;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function packagedClientDir(moduleUrl) {
|
|
37
|
+
const directory = resolve(packageRoot(moduleUrl), 'dist/client');
|
|
38
|
+
return existsSync(resolve(directory, 'index.html')) ? directory : undefined;
|
|
39
|
+
}
|
|
40
|
+
export async function packageVersion(moduleUrl) {
|
|
41
|
+
const manifest = JSON.parse(await readFile(resolve(packageRoot(moduleUrl), 'package.json'), 'utf8'));
|
|
42
|
+
if (typeof manifest.version !== 'string')
|
|
43
|
+
throw new Error('Package version is unavailable');
|
|
44
|
+
return manifest.version;
|
|
45
|
+
}
|
|
46
|
+
export async function probeCodexVersion() {
|
|
47
|
+
return runFile('codex', ['--version'])
|
|
48
|
+
.then(({ stdout }) => stdout.trim() || null)
|
|
49
|
+
.catch(() => null);
|
|
50
|
+
}
|
|
51
|
+
export function installShutdownHandlers(app, signalSource = process) {
|
|
52
|
+
let closing;
|
|
53
|
+
const shutdown = () => {
|
|
54
|
+
closing ??= app.close();
|
|
55
|
+
return closing;
|
|
56
|
+
};
|
|
57
|
+
const onSignal = () => {
|
|
58
|
+
signalSource.removeListener('SIGINT', onSignal);
|
|
59
|
+
signalSource.removeListener('SIGTERM', onSignal);
|
|
60
|
+
void shutdown().catch((error) => {
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
console.error(error instanceof Error ? error.message : 'Shutdown failed');
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
signalSource.once('SIGINT', onSignal);
|
|
66
|
+
signalSource.once('SIGTERM', onSignal);
|
|
67
|
+
return shutdown;
|
|
68
|
+
}
|
|
69
|
+
function parseInvocation(args, cwd) {
|
|
70
|
+
if (args.includes('--help')) {
|
|
71
|
+
if (args.length !== 1)
|
|
72
|
+
throw new CliUsageError('--help cannot be combined with other arguments');
|
|
73
|
+
return { command: 'help' };
|
|
74
|
+
}
|
|
75
|
+
if (args.includes('--version')) {
|
|
76
|
+
if (args.length !== 1)
|
|
77
|
+
throw new CliUsageError('--version cannot be combined with other arguments');
|
|
78
|
+
return { command: 'version' };
|
|
79
|
+
}
|
|
80
|
+
return { command: 'run', config: parseConfig(args, cwd) };
|
|
81
|
+
}
|
|
82
|
+
export async function runCli(dependencies = {}) {
|
|
83
|
+
const args = dependencies.args ?? process.argv.slice(2);
|
|
84
|
+
const cwd = dependencies.cwd ?? process.cwd();
|
|
85
|
+
const moduleUrl = dependencies.moduleUrl ?? import.meta.url;
|
|
86
|
+
const stdout = dependencies.stdout ?? process.stdout;
|
|
87
|
+
const stderr = dependencies.stderr ?? process.stderr;
|
|
88
|
+
let invocation;
|
|
89
|
+
try {
|
|
90
|
+
invocation = parseInvocation(args, cwd);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (!(error instanceof CliUsageError))
|
|
94
|
+
throw error;
|
|
95
|
+
stderr.write(`${error.message}\n\n${usage}\n`);
|
|
96
|
+
return error.exitCode;
|
|
97
|
+
}
|
|
98
|
+
if (invocation.command === 'help') {
|
|
99
|
+
stdout.write(`${usage}\n`);
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
if (invocation.command === 'version') {
|
|
103
|
+
stdout.write(`${await packageVersion(moduleUrl)}\n`);
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
const config = invocation.config;
|
|
107
|
+
const app = await (dependencies.compose ?? composeRelayApp)({
|
|
108
|
+
root: config.root,
|
|
109
|
+
dataDir: config.dataDir,
|
|
110
|
+
staticDir: packagedClientDir(moduleUrl),
|
|
111
|
+
profiles: new CodexProfileCatalog(),
|
|
112
|
+
installedCodexVersion: await (dependencies.probeCodexVersion ?? probeCodexVersion)(),
|
|
113
|
+
startAppServers: true,
|
|
114
|
+
});
|
|
115
|
+
installShutdownHandlers(app, dependencies.signalSource);
|
|
116
|
+
let address;
|
|
117
|
+
try {
|
|
118
|
+
address = await app.listen({ host: config.host, port: config.port });
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
await app.close();
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
stdout.write(`Gestalt Mobile listening on ${address}\n`);
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { join, resolve } from 'node:path';
|
|
10
|
+
import { buildApp } from './app.js';
|
|
11
|
+
import { FilesystemWorkspaceCatalog } from './platform/catalog/filesystem-workspace-catalog.js';
|
|
12
|
+
import { protocolCompatibility } from './platform/codex/protocol-compatibility.js';
|
|
13
|
+
import { launchCodexAppServer } from './platform/codex/codex-process-launcher.js';
|
|
14
|
+
import { createRecentThreadLister } from './platform/codex/recent-thread-lister.js';
|
|
15
|
+
import { CodexSessionRuntime } from './platform/codex/session-runtime.js';
|
|
16
|
+
import { normalizeCodexNotification } from './platform/codex/normalizer.js';
|
|
17
|
+
import { migrate } from './platform/persistence/migrate.js';
|
|
18
|
+
import { openRelayDatabase } from './platform/persistence/sqlite.js';
|
|
19
|
+
import { SqliteSessionRepository } from './platform/persistence/sqlite-session-repository.js';
|
|
20
|
+
import { SqliteEventJournal } from './platform/persistence/sqlite-event-journal.js';
|
|
21
|
+
import { SqlitePendingInteractionStore } from './platform/persistence/sqlite-pending-interaction-store.js';
|
|
22
|
+
import { SqliteIdempotencyStore } from './platform/persistence/sqlite-idempotency-store.js';
|
|
23
|
+
import { legacyRelayStatePath, relayStatePath } from './platform/persistence/state-path.js';
|
|
24
|
+
import { SessionEventBus } from './platform/events/session-event-bus.js';
|
|
25
|
+
import { checkoutBranch, cloneRepository, fetchUpstream, inspectGit, pullRebase, pushUpstream, } from './platform/git/git-inspector.js';
|
|
26
|
+
import { GitFetchCoordinator } from './platform/git/git-fetch-coordinator.js';
|
|
27
|
+
import { GitSummaryCache } from './platform/git/git-summary-cache.js';
|
|
28
|
+
import { SessionSupervisor } from './platform/runtime/session-supervisor.js';
|
|
29
|
+
import { mapWithConcurrency } from './platform/runtime/concurrency.js';
|
|
30
|
+
import { RelaySession } from './features/sessions/model/relay-session.js';
|
|
31
|
+
import { toPendingInteraction } from './platform/codex/server-request.js';
|
|
32
|
+
import { isValidInteractionResponse } from './features/sessions/interaction/response-validator.js';
|
|
33
|
+
import { promoteRecentThread } from './features/sessions/promote-recent-thread/use-case.js';
|
|
34
|
+
const generatedProtocolVersion = 'codex-cli 0.144.3';
|
|
35
|
+
export async function composeRelayApp(options) {
|
|
36
|
+
const root = resolve(options.root);
|
|
37
|
+
const databasePath = options.dataDir
|
|
38
|
+
? join(resolve(options.dataDir), 'relay.sqlite')
|
|
39
|
+
: resolveStateDatabasePath(root, process.env.XDG_STATE_HOME ?? join(homedir(), '.local', 'state'));
|
|
40
|
+
const database = openRelayDatabase(databasePath);
|
|
41
|
+
migrate(database);
|
|
42
|
+
const sessions = new SqliteSessionRepository(database);
|
|
43
|
+
const journal = new SqliteEventJournal(database);
|
|
44
|
+
const interactions = new SqlitePendingInteractionStore(database);
|
|
45
|
+
const idempotency = new SqliteIdempotencyStore(database);
|
|
46
|
+
const withPendingInteractions = (session) => (session ? { ...session, pendingInteractions: interactions.list(session.id) } : null);
|
|
47
|
+
const events = new SessionEventBus();
|
|
48
|
+
const workspaces = new FilesystemWorkspaceCatalog(root);
|
|
49
|
+
const recentThreads = createRecentThreadLister({
|
|
50
|
+
root,
|
|
51
|
+
profiles: options.profiles,
|
|
52
|
+
launch: options.launchAppServer ?? launchCodexAppServer,
|
|
53
|
+
});
|
|
54
|
+
const protocol = protocolCompatibility(options.installedCodexVersion, generatedProtocolVersion);
|
|
55
|
+
const gitFetches = new GitFetchCoordinator(fetchUpstream);
|
|
56
|
+
const gitSummaries = new GitSummaryCache(inspectGit);
|
|
57
|
+
let recoverExitedSession = () => { };
|
|
58
|
+
const runtime = options.startAppServers
|
|
59
|
+
? new CodexSessionRuntime(options.launchAppServer ?? launchCodexAppServer, undefined, (sessionId, notification) => {
|
|
60
|
+
const occurredAt = new Date().toISOString();
|
|
61
|
+
const normalized = normalizeCodexNotification(sessionId, 0, occurredAt, notification, sessions.find(sessionId)?.workspacePath);
|
|
62
|
+
if (!normalized)
|
|
63
|
+
return;
|
|
64
|
+
if (normalized.type === 'turnCompleted') {
|
|
65
|
+
const turnId = normalized.payload.turn?.id;
|
|
66
|
+
const session = sessions.find(sessionId);
|
|
67
|
+
if (session && turnId && session.activeTurnId === turnId)
|
|
68
|
+
sessions.save(RelaySession.rehydrate(session).completeTurn(turnId, occurredAt).snapshot);
|
|
69
|
+
}
|
|
70
|
+
events.publish(journal.append(sessionId, normalized.type, normalized.payload, normalized.occurredAt));
|
|
71
|
+
}, (sessionId, request) => {
|
|
72
|
+
const interaction = toPendingInteraction(request);
|
|
73
|
+
const session = withPendingInteractions(sessions.find(sessionId));
|
|
74
|
+
if (!interaction || !session)
|
|
75
|
+
return false;
|
|
76
|
+
interactions.add(sessionId, interaction);
|
|
77
|
+
const updated = RelaySession.rehydrate(session).requestInteraction(interaction, new Date().toISOString()).snapshot;
|
|
78
|
+
sessions.save(updated);
|
|
79
|
+
events.publish(journal.append(sessionId, 'interaction.requested', interaction, updated.updatedAt));
|
|
80
|
+
return true;
|
|
81
|
+
}, (sessionId) => recoverExitedSession(sessionId))
|
|
82
|
+
: null;
|
|
83
|
+
const saveSession = (session) => {
|
|
84
|
+
sessions.save(session);
|
|
85
|
+
events.publish(journal.append(session.id, 'session.updated', session, session.updatedAt));
|
|
86
|
+
};
|
|
87
|
+
if (runtime) {
|
|
88
|
+
const supervisor = new SessionSupervisor(async (sessionId) => {
|
|
89
|
+
const session = sessions.find(sessionId);
|
|
90
|
+
if (!session || session.desiredState !== 'active' || !session.threadId)
|
|
91
|
+
return;
|
|
92
|
+
const recovering = RelaySession.rehydrate(session).beginRecovery(new Date().toISOString()).snapshot;
|
|
93
|
+
saveSession(recovering);
|
|
94
|
+
saveSession(await runtime.restore(recovering, new Date().toISOString()));
|
|
95
|
+
}, (sessionId) => {
|
|
96
|
+
const session = sessions.find(sessionId);
|
|
97
|
+
if (session)
|
|
98
|
+
saveSession(RelaySession.rehydrate(session).requireAttention(new Date().toISOString()).snapshot);
|
|
99
|
+
});
|
|
100
|
+
recoverExitedSession = (sessionId) => supervisor.recover(sessionId);
|
|
101
|
+
}
|
|
102
|
+
const app = await buildApp({
|
|
103
|
+
health: {
|
|
104
|
+
async read() {
|
|
105
|
+
return {
|
|
106
|
+
status: protocol.compatible ? 'ok' : 'degraded',
|
|
107
|
+
version: '0.1.0',
|
|
108
|
+
codex: {
|
|
109
|
+
installedVersion: options.installedCodexVersion,
|
|
110
|
+
protocolVersion: generatedProtocolVersion,
|
|
111
|
+
compatible: protocol.compatible,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
logger: console,
|
|
117
|
+
staticDir: options.staticDir,
|
|
118
|
+
recentThreads,
|
|
119
|
+
bootstrap: {
|
|
120
|
+
workspaces,
|
|
121
|
+
profiles: options.profiles,
|
|
122
|
+
sessions: {
|
|
123
|
+
list: () => sessions.list().map((session) => withPendingInteractions(session)),
|
|
124
|
+
},
|
|
125
|
+
protocolCompatible: protocol.compatible,
|
|
126
|
+
},
|
|
127
|
+
sessionRoutes: {
|
|
128
|
+
createId: randomUUID,
|
|
129
|
+
now: () => new Date().toISOString(),
|
|
130
|
+
save: saveSession,
|
|
131
|
+
find: (id) => withPendingInteractions(sessions.find(id)),
|
|
132
|
+
list: () => sessions.list().map((session) => withPendingInteractions(session)),
|
|
133
|
+
workspaces,
|
|
134
|
+
profiles: options.profiles,
|
|
135
|
+
activate: runtime
|
|
136
|
+
? async (session, settings) => runtime.start(session, new Date().toISOString(), settings)
|
|
137
|
+
: undefined,
|
|
138
|
+
startTurn: runtime
|
|
139
|
+
? async (session, text) => runtime.startTurn(session, text, new Date().toISOString())
|
|
140
|
+
: undefined,
|
|
141
|
+
readHistory: runtime ? (session) => runtime.readHistory(session) : undefined,
|
|
142
|
+
currentSequence: (sessionId) => journal.since(sessionId, 0).at(-1)?.sequence ?? 0,
|
|
143
|
+
interruptTurn: runtime
|
|
144
|
+
? (session, turnId) => runtime.interruptTurn(session, turnId)
|
|
145
|
+
: undefined,
|
|
146
|
+
restore: runtime
|
|
147
|
+
? (session) => runtime.restore(session, new Date().toISOString())
|
|
148
|
+
: undefined,
|
|
149
|
+
promoteRecent: runtime
|
|
150
|
+
? (thread) => promoteRecentThread(thread, {
|
|
151
|
+
createId: randomUUID,
|
|
152
|
+
now: () => new Date().toISOString(),
|
|
153
|
+
list: () => sessions.list(),
|
|
154
|
+
save: saveSession,
|
|
155
|
+
restore: (session) => runtime.restore(session, new Date().toISOString()),
|
|
156
|
+
})
|
|
157
|
+
: undefined,
|
|
158
|
+
release: (session) => RelaySession.rehydrate(session).release(new Date().toISOString()).snapshot,
|
|
159
|
+
remove: (id) => sessions.remove(id),
|
|
160
|
+
idempotency,
|
|
161
|
+
close: runtime ? (id) => runtime.stop(id) : undefined,
|
|
162
|
+
replyInteraction: runtime
|
|
163
|
+
? (sessionId, requestId, value) => runtime.resolveServerRequest(sessionId, requestId, value)
|
|
164
|
+
: undefined,
|
|
165
|
+
interactionResolved: (sessionId, requestId, occurredAt) => {
|
|
166
|
+
events.publish(journal.append(sessionId, 'interaction.resolved', { requestId }, occurredAt));
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
sessionEvents: {
|
|
170
|
+
exists: (id) => sessions.find(id) !== null,
|
|
171
|
+
since: (id, after) => journal.since(id, after),
|
|
172
|
+
subscribe: (id, listener) => events.subscribe(id, listener),
|
|
173
|
+
},
|
|
174
|
+
interactions: {
|
|
175
|
+
resolve: (sessionId, requestId, resolvedAt) => interactions.resolve(sessionId, requestId, resolvedAt),
|
|
176
|
+
validate: (sessionId, requestId, value) => {
|
|
177
|
+
const interaction = interactions.find(sessionId, requestId);
|
|
178
|
+
if (!interaction)
|
|
179
|
+
return false;
|
|
180
|
+
return isValidInteractionResponse(interaction.kind, value);
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
gitSummary: {
|
|
184
|
+
inspect: async (path) => {
|
|
185
|
+
const summary = await gitSummaries.inspect(path);
|
|
186
|
+
const fetchedAt = gitFetches.lastSuccessfulAt(path);
|
|
187
|
+
return { ...summary, fetchedAt: fetchedAt ? new Date(fetchedAt).toISOString() : null };
|
|
188
|
+
},
|
|
189
|
+
inspectForPush: inspectGit,
|
|
190
|
+
push: async (path, upstream) => {
|
|
191
|
+
await pushUpstream(path, upstream);
|
|
192
|
+
gitSummaries.invalidate(path);
|
|
193
|
+
},
|
|
194
|
+
refresh: async (path) => {
|
|
195
|
+
await gitFetches.refresh(path);
|
|
196
|
+
gitSummaries.invalidate(path);
|
|
197
|
+
},
|
|
198
|
+
pull: async (path) => {
|
|
199
|
+
await pullRebase(path);
|
|
200
|
+
gitSummaries.invalidate(path);
|
|
201
|
+
},
|
|
202
|
+
checkout: async (path, branch) => {
|
|
203
|
+
await checkoutBranch(path, branch);
|
|
204
|
+
gitSummaries.invalidate(path);
|
|
205
|
+
},
|
|
206
|
+
clone: async (workspaceId, address) => cloneRepository((await workspaces.resolve(workspaceId)).realPath, address),
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
const restoreActiveSessions = async () => {
|
|
210
|
+
if (!runtime)
|
|
211
|
+
return;
|
|
212
|
+
await mapWithConcurrency(sessions
|
|
213
|
+
.list()
|
|
214
|
+
.filter((session) => session.desiredState === 'active' && session.threadId !== null), 2, async (session) => {
|
|
215
|
+
try {
|
|
216
|
+
saveSession(await runtime.restore(session, new Date().toISOString()));
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
saveSession(RelaySession.rehydrate(session).requireAttention(new Date().toISOString()).snapshot);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
};
|
|
223
|
+
if (runtime)
|
|
224
|
+
app.addHook('onListen', () => void restoreActiveSessions());
|
|
225
|
+
app.addHook('onClose', async () => {
|
|
226
|
+
runtime?.stopAll();
|
|
227
|
+
database.close();
|
|
228
|
+
});
|
|
229
|
+
return app;
|
|
230
|
+
}
|
|
231
|
+
function resolveStateDatabasePath(root, stateHome) {
|
|
232
|
+
const currentPath = relayStatePath(root, stateHome);
|
|
233
|
+
if (existsSync(currentPath))
|
|
234
|
+
return currentPath;
|
|
235
|
+
const legacyPath = legacyRelayStatePath(root, stateHome);
|
|
236
|
+
return existsSync(legacyPath) ? legacyPath : currentPath;
|
|
237
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
import { resolve } from 'node:path';
|
|
7
|
+
export class CliUsageError extends Error {
|
|
8
|
+
exitCode = 2;
|
|
9
|
+
}
|
|
10
|
+
const optionNames = new Set(['--cwd', '--host', '--port', '--data-dir']);
|
|
11
|
+
export function parseConfig(args, cwd = process.cwd()) {
|
|
12
|
+
const values = new Map();
|
|
13
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
14
|
+
const option = args[index];
|
|
15
|
+
if (!option.startsWith('--'))
|
|
16
|
+
throw new CliUsageError(`Unexpected argument: ${option}`);
|
|
17
|
+
if (!optionNames.has(option))
|
|
18
|
+
throw new CliUsageError(`Unknown option: ${option}`);
|
|
19
|
+
if (values.has(option))
|
|
20
|
+
throw new CliUsageError(`Duplicate option: ${option}`);
|
|
21
|
+
const value = args[index + 1];
|
|
22
|
+
if (!value || value.startsWith('--'))
|
|
23
|
+
throw new CliUsageError(`Missing value for ${option}`);
|
|
24
|
+
values.set(option, value);
|
|
25
|
+
index += 1;
|
|
26
|
+
}
|
|
27
|
+
const portValue = values.get('--port') ?? '3000';
|
|
28
|
+
const port = Number(portValue);
|
|
29
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
30
|
+
throw new CliUsageError(`Invalid --port: ${portValue}`);
|
|
31
|
+
return {
|
|
32
|
+
host: values.get('--host') ?? '127.0.0.1',
|
|
33
|
+
port,
|
|
34
|
+
root: resolve(cwd, values.get('--cwd') ?? '.'),
|
|
35
|
+
dataDir: values.get('--data-dir'),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
import { getBootstrap } from './use-case.js';
|
|
7
|
+
export function registerGetBootstrap(app, deps) {
|
|
8
|
+
app.get('/api/bootstrap', async () => getBootstrap(deps));
|
|
9
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
export async function getBootstrap(deps) {
|
|
7
|
+
const [workspaces, profiles] = await Promise.all([deps.workspaces.list(), deps.profiles.list()]);
|
|
8
|
+
return {
|
|
9
|
+
workspaces,
|
|
10
|
+
profiles,
|
|
11
|
+
sessions: deps.sessions.list(),
|
|
12
|
+
capabilities: {
|
|
13
|
+
approvals: true,
|
|
14
|
+
userInput: true,
|
|
15
|
+
git: true,
|
|
16
|
+
protocolCompatible: deps.protocolCompatible,
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
const requestSchema = z.object({ branch: z.string().min(1) });
|
|
8
|
+
export function registerCheckoutBranch(app, deps) {
|
|
9
|
+
app.post('/api/sessions/:id/git/checkout', async (request, reply) => {
|
|
10
|
+
const session = deps.find(request.params.id);
|
|
11
|
+
if (!session)
|
|
12
|
+
return reply.code(404).send({ code: 'SESSION_NOT_FOUND' });
|
|
13
|
+
const parsed = requestSchema.safeParse(request.body);
|
|
14
|
+
if (!parsed.success)
|
|
15
|
+
return reply.code(400).send({ code: 'INVALID_BRANCH' });
|
|
16
|
+
try {
|
|
17
|
+
await deps.checkout(session.workspacePath, parsed.data.branch);
|
|
18
|
+
return reply.code(202).send({ accepted: true });
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
return reply.code(409).send({
|
|
22
|
+
code: 'GIT_CHECKOUT_FAILED',
|
|
23
|
+
detail: error instanceof Error ? error.message : 'Git checkout failed.',
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
const requestSchema = z.object({
|
|
8
|
+
address: z.string().trim().min(1).max(2_048),
|
|
9
|
+
workspaceId: z.string().trim().min(1),
|
|
10
|
+
});
|
|
11
|
+
export function registerCloneRepository(app, deps) {
|
|
12
|
+
app.post('/api/git/clone', async (request, reply) => {
|
|
13
|
+
const parsed = requestSchema.safeParse(request.body);
|
|
14
|
+
if (!parsed.success)
|
|
15
|
+
return reply.code(400).send({ code: 'INVALID_CLONE_ADDRESS' });
|
|
16
|
+
await deps.clone(parsed.data.workspaceId, parsed.data.address);
|
|
17
|
+
return reply.code(202).send({ accepted: true });
|
|
18
|
+
});
|
|
19
|
+
}
|