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,29 @@
|
|
|
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
|
+
threadId: z.string().min(1),
|
|
9
|
+
cwd: z.string().startsWith('/'),
|
|
10
|
+
});
|
|
11
|
+
export function registerPromoteRecentThread(app, deps) {
|
|
12
|
+
app.post('/api/sessions/recent-threads/open', async (request, reply) => {
|
|
13
|
+
const parsed = requestSchema.safeParse(request.body);
|
|
14
|
+
if (!parsed.success)
|
|
15
|
+
return reply.code(400).send({ code: 'INVALID_RECENT_THREAD' });
|
|
16
|
+
const thread = (await deps.list()).find((candidate) => candidate.id === parsed.data.threadId && candidate.cwd === parsed.data.cwd);
|
|
17
|
+
if (!thread)
|
|
18
|
+
return reply.code(404).send({ code: 'RECENT_THREAD_NOT_FOUND' });
|
|
19
|
+
try {
|
|
20
|
+
return reply.code(202).send(await deps.promote(thread));
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
return reply.code(409).send({
|
|
24
|
+
code: 'RECENT_THREAD_OPEN_FAILED',
|
|
25
|
+
detail: error instanceof Error ? error.message : 'Could not open the recent session.',
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
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 { RelaySession } from '../model/relay-session.js';
|
|
7
|
+
export async function promoteRecentThread(thread, deps) {
|
|
8
|
+
const existing = deps.list().find((session) => session.threadId === thread.id);
|
|
9
|
+
if (existing?.state === 'ready' || existing?.state === 'turnActive')
|
|
10
|
+
return existing;
|
|
11
|
+
const now = deps.now();
|
|
12
|
+
const imported = existing ??
|
|
13
|
+
RelaySession.fromExistingThread({
|
|
14
|
+
id: deps.createId(),
|
|
15
|
+
workspaceId: thread.cwd,
|
|
16
|
+
workspacePath: thread.cwd,
|
|
17
|
+
profile: 'default',
|
|
18
|
+
threadId: thread.id,
|
|
19
|
+
now,
|
|
20
|
+
}).snapshot;
|
|
21
|
+
if (!existing)
|
|
22
|
+
deps.save(imported);
|
|
23
|
+
const active = await deps.restore(imported);
|
|
24
|
+
deps.save(active);
|
|
25
|
+
return active;
|
|
26
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
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 { idempotencyKey } from '../../../platform/http/idempotency.js';
|
|
7
|
+
export function registerReleaseSession(app, deps) {
|
|
8
|
+
app.post('/api/sessions/:id/release', async (request, reply) => {
|
|
9
|
+
const id = request.params.id;
|
|
10
|
+
const key = idempotencyKey(request.headers);
|
|
11
|
+
const scope = `release-session:${id}`;
|
|
12
|
+
const prior = key ? deps.idempotency?.get(scope, key) : null;
|
|
13
|
+
if (prior)
|
|
14
|
+
return reply.code(prior.statusCode).send(JSON.parse(prior.body));
|
|
15
|
+
const session = deps.find(id);
|
|
16
|
+
if (!session)
|
|
17
|
+
return reply.code(404).send({ code: 'SESSION_NOT_FOUND' });
|
|
18
|
+
const released = deps.release(session);
|
|
19
|
+
deps.save(released);
|
|
20
|
+
deps.close(id);
|
|
21
|
+
if (key)
|
|
22
|
+
deps.idempotency?.put(scope, key, 202, JSON.stringify(released));
|
|
23
|
+
return reply.code(202).send(released);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -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 { RelaySession } from '../model/relay-session.js';
|
|
7
|
+
export function release(session, now) {
|
|
8
|
+
return RelaySession.rehydrate(session).release(now).snapshot;
|
|
9
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
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 function registerRespondInteraction(app, deps) {
|
|
7
|
+
app.post('/api/sessions/:id/interactions/:requestId', async (request, reply) => {
|
|
8
|
+
const { id, requestId } = request.params;
|
|
9
|
+
if (!deps.exists(id))
|
|
10
|
+
return reply.code(404).send({ code: 'SESSION_NOT_FOUND' });
|
|
11
|
+
const value = request.body;
|
|
12
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
13
|
+
return reply.code(400).send({ code: 'INTERACTION_RESPONSE_INVALID' });
|
|
14
|
+
if (deps.validate && !deps.validate(id, requestId, value))
|
|
15
|
+
return reply.code(400).send({ code: 'INTERACTION_RESPONSE_INVALID' });
|
|
16
|
+
if (!deps.reply(id, requestId, value))
|
|
17
|
+
return reply.code(409).send({ code: 'INTERACTION_NOT_PENDING' });
|
|
18
|
+
const resolvedAt = deps.now();
|
|
19
|
+
if (!deps.resolve(id, requestId, resolvedAt))
|
|
20
|
+
return reply.code(409).send({ code: 'INTERACTION_NOT_PENDING' });
|
|
21
|
+
deps.resolved?.(id, requestId, resolvedAt);
|
|
22
|
+
return reply.code(202).send({ accepted: true });
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -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 { idempotencyKey } from '../../../platform/http/idempotency.js';
|
|
7
|
+
import { canRestore } from './use-case.js';
|
|
8
|
+
export function registerRestoreSession(app, deps) {
|
|
9
|
+
app.post('/api/sessions/:id/restore', async (request, reply) => {
|
|
10
|
+
const id = request.params.id;
|
|
11
|
+
const key = idempotencyKey(request.headers);
|
|
12
|
+
const scope = `restore-session:${id}`;
|
|
13
|
+
const prior = key ? deps.idempotency?.get(scope, key) : null;
|
|
14
|
+
if (prior)
|
|
15
|
+
return reply.code(prior.statusCode).send(JSON.parse(prior.body));
|
|
16
|
+
const session = deps.find(id);
|
|
17
|
+
if (!session)
|
|
18
|
+
return reply.code(404).send({ code: 'SESSION_NOT_FOUND' });
|
|
19
|
+
if (!canRestore(session))
|
|
20
|
+
return reply.code(409).send({ code: 'SESSION_CANNOT_RESTORE' });
|
|
21
|
+
const restored = await deps.restore(session);
|
|
22
|
+
deps.save(restored);
|
|
23
|
+
if (key)
|
|
24
|
+
deps.idempotency?.put(scope, key, 200, JSON.stringify(restored));
|
|
25
|
+
return reply.send(restored);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
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 { RelaySession } from '../model/relay-session.js';
|
|
7
|
+
export function canRestore(session) {
|
|
8
|
+
return (session.threadId !== null &&
|
|
9
|
+
(session.state === 'stopped' ||
|
|
10
|
+
session.state === 'released' ||
|
|
11
|
+
session.state === 'attentionRequired'));
|
|
12
|
+
}
|
|
13
|
+
export function restore(session, now) {
|
|
14
|
+
if (!canRestore(session))
|
|
15
|
+
throw new Error('SESSION_CANNOT_RESTORE');
|
|
16
|
+
return RelaySession.rehydrate(session).beginRecovery(now).restore(now).snapshot;
|
|
17
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
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 { WebSocketServer } from 'ws';
|
|
7
|
+
import { replayOrResync } from '../../../platform/events/replay.js';
|
|
8
|
+
import { isSlowClient } from '../../../platform/events/slow-client.js';
|
|
9
|
+
import { installWebSocketHeartbeat } from '../../../platform/events/websocket-heartbeat.js';
|
|
10
|
+
export function registerSessionEvents(app, deps) {
|
|
11
|
+
const server = new WebSocketServer({ noServer: true });
|
|
12
|
+
app.server.on('upgrade', (request, socket, head) => {
|
|
13
|
+
const url = new URL(request.url ?? '/', 'http://relay.invalid');
|
|
14
|
+
const match = /^\/api\/sessions\/([^/]+)\/events$/.exec(url.pathname);
|
|
15
|
+
const after = Number(url.searchParams.get('after') ?? '0');
|
|
16
|
+
if (!match || !Number.isSafeInteger(after) || after < 0 || !deps.exists(match[1])) {
|
|
17
|
+
socket.destroy();
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
server.handleUpgrade(request, socket, head, (connection) => {
|
|
21
|
+
const sessionId = match[1];
|
|
22
|
+
const send = (event) => {
|
|
23
|
+
if (isSlowClient(connection.bufferedAmount))
|
|
24
|
+
return connection.close(1013, 'slow client');
|
|
25
|
+
connection.send(JSON.stringify({ type: 'relay.event', event }));
|
|
26
|
+
};
|
|
27
|
+
const replay = replayOrResync(deps.since(sessionId, after), after);
|
|
28
|
+
if (replay.kind === 'resync') {
|
|
29
|
+
const currentSequence = deps.since(sessionId, 0).at(-1)?.sequence ?? after;
|
|
30
|
+
connection.send(JSON.stringify({ type: 'relay.resyncRequired', currentSequence }));
|
|
31
|
+
connection.close();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
replay.events.forEach(send);
|
|
35
|
+
const unsubscribe = deps.subscribe(sessionId, send);
|
|
36
|
+
const removeHeartbeat = installWebSocketHeartbeat(connection);
|
|
37
|
+
connection.on('close', () => {
|
|
38
|
+
removeHeartbeat();
|
|
39
|
+
unsubscribe();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
app.addHook('onClose', async () => server.close());
|
|
44
|
+
}
|
|
@@ -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 { idempotencyKey } from '../../../platform/http/idempotency.js';
|
|
7
|
+
import { parseStartSessionRequest } from './request.js';
|
|
8
|
+
import { startSession } from './use-case.js';
|
|
9
|
+
export function registerStartSession(app, deps) {
|
|
10
|
+
app.post('/api/sessions', async (request, reply) => {
|
|
11
|
+
const key = idempotencyKey(request.headers);
|
|
12
|
+
const prior = key ? deps.idempotency?.get('start-session', key) : null;
|
|
13
|
+
if (prior)
|
|
14
|
+
return reply.code(prior.statusCode).send(JSON.parse(prior.body));
|
|
15
|
+
const body = parseStartSessionRequest(request.body);
|
|
16
|
+
if (!body)
|
|
17
|
+
return reply.code(400).type('application/problem+json').send({
|
|
18
|
+
type: 'urn:gestalt-mobile:error:invalid-start-session-request',
|
|
19
|
+
title: 'Invalid start session request',
|
|
20
|
+
status: 400,
|
|
21
|
+
detail: 'Workspace, profile, and Codex session settings must be valid.',
|
|
22
|
+
code: 'INVALID_START_SESSION_REQUEST',
|
|
23
|
+
retryable: false,
|
|
24
|
+
});
|
|
25
|
+
let started;
|
|
26
|
+
try {
|
|
27
|
+
started = await startSession(body, deps);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
deps.reportFailure?.('start-session', error);
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
if (key)
|
|
34
|
+
deps.idempotency?.put('start-session', key, 202, JSON.stringify(started));
|
|
35
|
+
return reply.code(202).send(started);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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 schema = z.object({
|
|
8
|
+
workspaceId: z.string().min(1),
|
|
9
|
+
profile: z.string().min(1),
|
|
10
|
+
model: z
|
|
11
|
+
.string()
|
|
12
|
+
.trim()
|
|
13
|
+
.optional()
|
|
14
|
+
.transform((value) => value || undefined),
|
|
15
|
+
sandbox: z.enum(['read-only', 'workspace-write', 'danger-full-access']).optional(),
|
|
16
|
+
approvalPolicy: z.enum(['untrusted', 'on-request', 'never']).optional(),
|
|
17
|
+
});
|
|
18
|
+
/** Parses only settings that are safe to send directly to Codex thread/start. */
|
|
19
|
+
export function parseStartSessionRequest(input) {
|
|
20
|
+
const result = schema.safeParse(input);
|
|
21
|
+
return result.success ? result.data : null;
|
|
22
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
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 { RelaySession } from '../model/relay-session.js';
|
|
7
|
+
export async function startSession(input, deps) {
|
|
8
|
+
const [workspace] = await Promise.all([
|
|
9
|
+
deps.workspaces.resolve(input.workspaceId),
|
|
10
|
+
deps.profiles.require(input.profile),
|
|
11
|
+
]);
|
|
12
|
+
const session = RelaySession.create({
|
|
13
|
+
id: deps.createId(),
|
|
14
|
+
workspaceId: workspace.id,
|
|
15
|
+
workspacePath: workspace.realPath,
|
|
16
|
+
profile: input.profile,
|
|
17
|
+
now: deps.now(),
|
|
18
|
+
}).snapshot;
|
|
19
|
+
deps.save(session);
|
|
20
|
+
if (!deps.activate)
|
|
21
|
+
return session;
|
|
22
|
+
const active = await deps.activate(session, {
|
|
23
|
+
model: input.model,
|
|
24
|
+
sandbox: input.sandbox,
|
|
25
|
+
approvalPolicy: input.approvalPolicy,
|
|
26
|
+
});
|
|
27
|
+
deps.save(active);
|
|
28
|
+
return active;
|
|
29
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
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 function registerStartTurn(app, deps) {
|
|
7
|
+
app.post('/api/sessions/:id/turns', async (request, reply) => {
|
|
8
|
+
const session = deps.find(request.params.id);
|
|
9
|
+
if (!session)
|
|
10
|
+
return reply.code(404).send({ code: 'SESSION_NOT_FOUND' });
|
|
11
|
+
const text = request.body.text?.trim() ?? '';
|
|
12
|
+
if (text.length > 100_000)
|
|
13
|
+
return reply.code(400).send({ code: 'TURN_INPUT_TOO_LONG' });
|
|
14
|
+
if (!text || session.state !== 'ready')
|
|
15
|
+
return reply.code(409).send({ code: 'SESSION_NOT_READY' });
|
|
16
|
+
const started = await deps.start(session, text);
|
|
17
|
+
deps.save(started);
|
|
18
|
+
return reply.code(202).send(started);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
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 { idempotencyKey } from '../../../platform/http/idempotency.js';
|
|
7
|
+
export function registerStopSession(app, deps) {
|
|
8
|
+
app.post('/api/sessions/:id/stop', async (request, reply) => {
|
|
9
|
+
const id = request.params.id;
|
|
10
|
+
const key = idempotencyKey(request.headers);
|
|
11
|
+
const scope = `stop-session:${id}`;
|
|
12
|
+
const prior = key ? deps.idempotency?.get(scope, key) : null;
|
|
13
|
+
if (prior)
|
|
14
|
+
return reply.code(prior.statusCode).send(JSON.parse(prior.body));
|
|
15
|
+
const session = deps.find(id);
|
|
16
|
+
if (!session)
|
|
17
|
+
return reply.code(404).send({ code: 'SESSION_NOT_FOUND' });
|
|
18
|
+
const stopped = deps.stop(session);
|
|
19
|
+
deps.save(stopped);
|
|
20
|
+
deps.close(id);
|
|
21
|
+
if (key)
|
|
22
|
+
deps.idempotency?.put(scope, key, 202, JSON.stringify(stopped));
|
|
23
|
+
return reply.code(202).send(stopped);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -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 { RelaySession } from '../model/relay-session.js';
|
|
7
|
+
export function stop(session, now) {
|
|
8
|
+
return RelaySession.rehydrate(session).stop(now).snapshot;
|
|
9
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
4
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
5
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
*/
|
|
7
|
+
import { runCli } from './cli.js';
|
|
8
|
+
try {
|
|
9
|
+
process.exitCode = await runCli();
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
console.error(error instanceof Error ? error.message : 'Gestalt Mobile startup failed');
|
|
13
|
+
process.exitCode = 1;
|
|
14
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
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 { homedir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { promisify } from 'node:util';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
const execute = promisify(execFile);
|
|
13
|
+
const statusSchema = z.object({
|
|
14
|
+
profiles: z.array(z.object({
|
|
15
|
+
name: z.string(),
|
|
16
|
+
state: z.enum(['ok', 'not_logged_in', 'error']),
|
|
17
|
+
status: z.string(),
|
|
18
|
+
home: z.string().optional(),
|
|
19
|
+
})),
|
|
20
|
+
});
|
|
21
|
+
export class CodexProfileCatalog {
|
|
22
|
+
command;
|
|
23
|
+
defaultHomeExists;
|
|
24
|
+
constructor(command = async () => (await execute('codex-profile', ['status', '--json'])).stdout, defaultHomeExists = () => existsSync(join(homedir(), '.codex'))) {
|
|
25
|
+
this.command = command;
|
|
26
|
+
this.defaultHomeExists = defaultHomeExists;
|
|
27
|
+
}
|
|
28
|
+
async list() {
|
|
29
|
+
const profiles = statusSchema
|
|
30
|
+
.parse(JSON.parse(await this.command()))
|
|
31
|
+
.profiles.map(({ name, state, status }) => ({ name, state, status }));
|
|
32
|
+
return profiles.length || !this.defaultHomeExists()
|
|
33
|
+
? profiles
|
|
34
|
+
: [{ name: 'default', state: 'ok', status: 'Using ~/.codex' }];
|
|
35
|
+
}
|
|
36
|
+
async require(name) {
|
|
37
|
+
const profile = (await this.list()).find((item) => item.name === name);
|
|
38
|
+
if (!profile)
|
|
39
|
+
throw new Error('PROFILE_NOT_FOUND');
|
|
40
|
+
return profile;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
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 { createHash } from 'node:crypto';
|
|
7
|
+
import { readdir, realpath, stat } from 'node:fs/promises';
|
|
8
|
+
import { join, relative } from 'node:path';
|
|
9
|
+
export class FilesystemWorkspaceCatalog {
|
|
10
|
+
root;
|
|
11
|
+
constructor(root) {
|
|
12
|
+
this.root = realpath(root);
|
|
13
|
+
}
|
|
14
|
+
async list() {
|
|
15
|
+
return (await this.entries()).map(({ id, name, git }) => ({ id, name, isGitRepository: git }));
|
|
16
|
+
}
|
|
17
|
+
async resolve(id) {
|
|
18
|
+
const found = (await this.entries()).find((entry) => entry.id === id);
|
|
19
|
+
if (!found)
|
|
20
|
+
throw new Error('WORKSPACE_NOT_FOUND');
|
|
21
|
+
return { id: found.id, name: found.name, realPath: found.path };
|
|
22
|
+
}
|
|
23
|
+
async entries() {
|
|
24
|
+
const root = await this.root;
|
|
25
|
+
const children = await readdir(root, { withFileTypes: true });
|
|
26
|
+
const rootEntry = {
|
|
27
|
+
id: createHash('sha256').update(root).digest('base64url'),
|
|
28
|
+
name: '/',
|
|
29
|
+
path: root,
|
|
30
|
+
git: await stat(join(root, '.git'))
|
|
31
|
+
.then(() => true)
|
|
32
|
+
.catch(() => false),
|
|
33
|
+
};
|
|
34
|
+
const output = await Promise.all(children.map(async (child) => {
|
|
35
|
+
if (!child.isDirectory() && !child.isSymbolicLink())
|
|
36
|
+
return null;
|
|
37
|
+
try {
|
|
38
|
+
const path = await realpath(join(root, child.name));
|
|
39
|
+
const info = await stat(path);
|
|
40
|
+
if (!info.isDirectory() || relative(root, path).startsWith('..'))
|
|
41
|
+
return null;
|
|
42
|
+
const git = await stat(join(path, '.git'))
|
|
43
|
+
.then(() => true)
|
|
44
|
+
.catch(() => false);
|
|
45
|
+
return {
|
|
46
|
+
id: createHash('sha256').update(path).digest('base64url'),
|
|
47
|
+
name: child.name,
|
|
48
|
+
path,
|
|
49
|
+
git,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}));
|
|
56
|
+
return [rootEntry, ...output]
|
|
57
|
+
.filter((value) => value !== null)
|
|
58
|
+
.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -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 { spawnSync } from 'node:child_process';
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
const localAvailability = {
|
|
11
|
+
codexProfileAvailable: () => !spawnSync('codex-profile', ['--version'], { stdio: 'ignore' }).error,
|
|
12
|
+
gestaltHomeExists: () => existsSync(join(homedir(), '.codex-gestalt')),
|
|
13
|
+
};
|
|
14
|
+
export function profileAppServerCommand(_profile, availability = localAvailability) {
|
|
15
|
+
void _profile;
|
|
16
|
+
return availability.codexProfileAvailable() && availability.gestaltHomeExists()
|
|
17
|
+
? { command: 'codex-profile', args: ['cli', 'gestalt', 'app-server', '--stdio'] }
|
|
18
|
+
: { command: 'codex', args: ['app-server', '--stdio'] };
|
|
19
|
+
}
|
|
@@ -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 { createHash } from 'node:crypto';
|
|
7
|
+
export function workspaceId(path) {
|
|
8
|
+
return createHash('sha256').update(path).digest('base64url');
|
|
9
|
+
}
|
|
@@ -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 { spawn } from 'node:child_process';
|
|
7
|
+
import { JsonRpcClient } from './json-rpc-client.js';
|
|
8
|
+
import { profileAppServerCommand } from '../catalog/profile-command.js';
|
|
9
|
+
export function launchCodexAppServer(input) {
|
|
10
|
+
const launch = profileAppServerCommand(input.profile);
|
|
11
|
+
const child = spawn(launch.command, launch.args, {
|
|
12
|
+
cwd: input.cwd,
|
|
13
|
+
shell: false,
|
|
14
|
+
stdio: 'pipe',
|
|
15
|
+
});
|
|
16
|
+
const rpc = new JsonRpcClient(child.stdout, child.stdin);
|
|
17
|
+
child.once('error', (error) => rpc.fail(error));
|
|
18
|
+
return {
|
|
19
|
+
child,
|
|
20
|
+
rpc,
|
|
21
|
+
close: () => child.kill('SIGTERM'),
|
|
22
|
+
onExit: (listener) => {
|
|
23
|
+
child.once('exit', listener);
|
|
24
|
+
return () => child.off('exit', listener);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
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 function protocolErrorCode(installed, expected) {
|
|
7
|
+
if (!installed)
|
|
8
|
+
return 'CODEX_NOT_FOUND';
|
|
9
|
+
return installed === expected ? null : 'CODEX_PROTOCOL_MISMATCH';
|
|
10
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
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 { createInterface } from 'node:readline';
|
|
7
|
+
export class JsonRpcClient {
|
|
8
|
+
output;
|
|
9
|
+
sequence = 0;
|
|
10
|
+
pending = new Map();
|
|
11
|
+
notifications = new Set();
|
|
12
|
+
serverRequests = new Set();
|
|
13
|
+
failure = null;
|
|
14
|
+
constructor(input, output) {
|
|
15
|
+
this.output = output;
|
|
16
|
+
createInterface({ input, crlfDelay: Infinity }).on('line', (line) => this.receive(line));
|
|
17
|
+
}
|
|
18
|
+
request(method, params) {
|
|
19
|
+
if (this.failure)
|
|
20
|
+
return Promise.reject(this.failure);
|
|
21
|
+
const id = ++this.sequence;
|
|
22
|
+
this.output.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`);
|
|
23
|
+
return new Promise((resolve, reject) => this.pending.set(id, { resolve, reject }));
|
|
24
|
+
}
|
|
25
|
+
fail(error) {
|
|
26
|
+
if (this.failure)
|
|
27
|
+
return;
|
|
28
|
+
this.failure = error;
|
|
29
|
+
for (const { reject } of this.pending.values())
|
|
30
|
+
reject(error);
|
|
31
|
+
this.pending.clear();
|
|
32
|
+
}
|
|
33
|
+
onNotification(listener) {
|
|
34
|
+
this.notifications.add(listener);
|
|
35
|
+
return () => this.notifications.delete(listener);
|
|
36
|
+
}
|
|
37
|
+
onServerRequest(listener) {
|
|
38
|
+
this.serverRequests.add(listener);
|
|
39
|
+
return () => this.serverRequests.delete(listener);
|
|
40
|
+
}
|
|
41
|
+
receive(line) {
|
|
42
|
+
try {
|
|
43
|
+
const message = JSON.parse(line);
|
|
44
|
+
if (message.id === undefined) {
|
|
45
|
+
if (message.method)
|
|
46
|
+
this.notifications.forEach((listener) => listener({ method: message.method, params: message.params }));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (message.method) {
|
|
50
|
+
const listener = this.serverRequests.values().next().value;
|
|
51
|
+
if (!listener)
|
|
52
|
+
return;
|
|
53
|
+
void Promise.resolve(listener({ id: message.id, method: message.method, params: message.params }))
|
|
54
|
+
.then((result) => this.output.write(`${JSON.stringify({ jsonrpc: '2.0', id: message.id, result })}\n`))
|
|
55
|
+
.catch((error) => this.output.write(`${JSON.stringify({ jsonrpc: '2.0', id: message.id, error: { message: error instanceof Error ? error.message : 'REQUEST_FAILED' } })}\n`));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const pending = this.pending.get(message.id);
|
|
59
|
+
if (!pending)
|
|
60
|
+
return;
|
|
61
|
+
this.pending.delete(message.id);
|
|
62
|
+
if (message.error)
|
|
63
|
+
pending.reject(message.error);
|
|
64
|
+
else
|
|
65
|
+
pending.resolve(message.result);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
/* malformed protocol messages are ignored at this boundary */
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|