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.
Files changed (112) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +133 -0
  3. package/dist/client/assets/index-0wjiXBGW.js +10 -0
  4. package/dist/client/assets/index-CfoipqOl.css +1 -0
  5. package/dist/client/branding/dyne-logotype-black.svg +17 -0
  6. package/dist/client/branding/dyne-logotype-white.svg +17 -0
  7. package/dist/client/branding/p_glogo_grey.svg +8 -0
  8. package/dist/client/branding/p_glogo_white.svg +8 -0
  9. package/dist/client/branding/t_glogo_grey.svg +9 -0
  10. package/dist/client/branding/t_glogo_white.svg +9 -0
  11. package/dist/client/index.html +19 -0
  12. package/dist/server/server/app.js +148 -0
  13. package/dist/server/server/cli/list-sessions.js +13 -0
  14. package/dist/server/server/cli.js +126 -0
  15. package/dist/server/server/composition.js +237 -0
  16. package/dist/server/server/config.js +37 -0
  17. package/dist/server/server/features/catalog/application/ports.js +6 -0
  18. package/dist/server/server/features/catalog/get-bootstrap/endpoint.js +9 -0
  19. package/dist/server/server/features/catalog/get-bootstrap/request.js +6 -0
  20. package/dist/server/server/features/catalog/get-bootstrap/response.js +6 -0
  21. package/dist/server/server/features/catalog/get-bootstrap/use-case.js +19 -0
  22. package/dist/server/server/features/git/checkout-branch/endpoint.js +27 -0
  23. package/dist/server/server/features/git/clone-repository/endpoint.js +19 -0
  24. package/dist/server/server/features/git/get-summary/endpoint.js +13 -0
  25. package/dist/server/server/features/git/pull-rebase/endpoint.js +22 -0
  26. package/dist/server/server/features/git/push-policy.js +14 -0
  27. package/dist/server/server/features/git/push-upstream/endpoint.js +29 -0
  28. package/dist/server/server/features/git/refresh/endpoint.js +24 -0
  29. package/dist/server/server/features/git/refresh-policy.js +8 -0
  30. package/dist/server/server/features/health/get-health/endpoint.js +8 -0
  31. package/dist/server/server/features/health/get-health/request.js +6 -0
  32. package/dist/server/server/features/health/get-health/response.js +6 -0
  33. package/dist/server/server/features/sessions/application/ports.js +6 -0
  34. package/dist/server/server/features/sessions/application/resume-command.js +22 -0
  35. package/dist/server/server/features/sessions/application/start-settings.js +6 -0
  36. package/dist/server/server/features/sessions/forget-session/endpoint.js +15 -0
  37. package/dist/server/server/features/sessions/get-history/endpoint.js +20 -0
  38. package/dist/server/server/features/sessions/get-history/history-mapper.js +82 -0
  39. package/dist/server/server/features/sessions/get-session/endpoint.js +17 -0
  40. package/dist/server/server/features/sessions/history/use-case.js +8 -0
  41. package/dist/server/server/features/sessions/interaction/kind.js +8 -0
  42. package/dist/server/server/features/sessions/interaction/response-guard.js +8 -0
  43. package/dist/server/server/features/sessions/interaction/response-validator.js +35 -0
  44. package/dist/server/server/features/sessions/interrupt-turn/endpoint.js +19 -0
  45. package/dist/server/server/features/sessions/interrupt-turn/use-case.js +10 -0
  46. package/dist/server/server/features/sessions/lifecycle/use-case.js +15 -0
  47. package/dist/server/server/features/sessions/list-recent-threads/endpoint.js +11 -0
  48. package/dist/server/server/features/sessions/list-sessions/endpoint.js +12 -0
  49. package/dist/server/server/features/sessions/list-sessions/use-case.js +8 -0
  50. package/dist/server/server/features/sessions/model/errors.js +13 -0
  51. package/dist/server/server/features/sessions/model/events.js +6 -0
  52. package/dist/server/server/features/sessions/model/relay-session.js +122 -0
  53. package/dist/server/server/features/sessions/model/value-objects.js +22 -0
  54. package/dist/server/server/features/sessions/promote-recent-thread/endpoint.js +29 -0
  55. package/dist/server/server/features/sessions/promote-recent-thread/use-case.js +26 -0
  56. package/dist/server/server/features/sessions/release-session/endpoint.js +25 -0
  57. package/dist/server/server/features/sessions/release-session/use-case.js +9 -0
  58. package/dist/server/server/features/sessions/respond-interaction/endpoint.js +24 -0
  59. package/dist/server/server/features/sessions/restore-session/endpoint.js +27 -0
  60. package/dist/server/server/features/sessions/restore-session/use-case.js +17 -0
  61. package/dist/server/server/features/sessions/session-events/endpoint.js +44 -0
  62. package/dist/server/server/features/sessions/start-session/endpoint.js +37 -0
  63. package/dist/server/server/features/sessions/start-session/request.js +22 -0
  64. package/dist/server/server/features/sessions/start-session/use-case.js +29 -0
  65. package/dist/server/server/features/sessions/start-turn/endpoint.js +20 -0
  66. package/dist/server/server/features/sessions/stop-session/endpoint.js +25 -0
  67. package/dist/server/server/features/sessions/stop-session/use-case.js +9 -0
  68. package/dist/server/server/features/sessions/turn/start-policy.js +8 -0
  69. package/dist/server/server/index.js +7 -0
  70. package/dist/server/server/main.js +14 -0
  71. package/dist/server/server/platform/catalog/codex-profile-catalog.js +42 -0
  72. package/dist/server/server/platform/catalog/filesystem-workspace-catalog.js +60 -0
  73. package/dist/server/server/platform/catalog/profile-command.js +19 -0
  74. package/dist/server/server/platform/catalog/workspace-id.js +9 -0
  75. package/dist/server/server/platform/codex/approval-policy.js +8 -0
  76. package/dist/server/server/platform/codex/codex-process-launcher.js +27 -0
  77. package/dist/server/server/platform/codex/error-code.js +10 -0
  78. package/dist/server/server/platform/codex/json-rpc-client.js +71 -0
  79. package/dist/server/server/platform/codex/normalizer.js +81 -0
  80. package/dist/server/server/platform/codex/protocol-compatibility.js +12 -0
  81. package/dist/server/server/platform/codex/recent-thread-lister.js +49 -0
  82. package/dist/server/server/platform/codex/server-request.js +14 -0
  83. package/dist/server/server/platform/codex/session-runtime.js +142 -0
  84. package/dist/server/server/platform/codex/thread-id.js +10 -0
  85. package/dist/server/server/platform/events/replay.js +12 -0
  86. package/dist/server/server/platform/events/session-event-bus.js +17 -0
  87. package/dist/server/server/platform/events/slow-client.js +9 -0
  88. package/dist/server/server/platform/events/websocket-heartbeat.js +20 -0
  89. package/dist/server/server/platform/git/command.js +13 -0
  90. package/dist/server/server/platform/git/git-fetch-coordinator.js +40 -0
  91. package/dist/server/server/platform/git/git-inspector.js +116 -0
  92. package/dist/server/server/platform/git/git-summary-cache.js +26 -0
  93. package/dist/server/server/platform/git/git-summary.js +8 -0
  94. package/dist/server/server/platform/http/idempotency.js +9 -0
  95. package/dist/server/server/platform/http/problem-handler.js +32 -0
  96. package/dist/server/server/platform/http/problem.js +15 -0
  97. package/dist/server/server/platform/observability/safe-log.js +9 -0
  98. package/dist/server/server/platform/persistence/migrate.js +9 -0
  99. package/dist/server/server/platform/persistence/retention.js +9 -0
  100. package/dist/server/server/platform/persistence/sqlite-event-journal.js +50 -0
  101. package/dist/server/server/platform/persistence/sqlite-idempotency-store.js +22 -0
  102. package/dist/server/server/platform/persistence/sqlite-pending-interaction-store.js +33 -0
  103. package/dist/server/server/platform/persistence/sqlite-session-repository.js +43 -0
  104. package/dist/server/server/platform/persistence/sqlite.js +14 -0
  105. package/dist/server/server/platform/persistence/state-path.js +16 -0
  106. package/dist/server/server/platform/runtime/clock.js +9 -0
  107. package/dist/server/server/platform/runtime/concurrency.js +15 -0
  108. package/dist/server/server/platform/runtime/retry-policy.js +9 -0
  109. package/dist/server/server/platform/runtime/session-supervisor.js +41 -0
  110. package/dist/server/shared/contracts/problem.js +6 -0
  111. package/dist/server/shared/contracts/session-event.js +6 -0
  112. 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,8 @@
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 mayStartTurn(state, text) {
7
+ return state === 'ready' && text.trim().length > 0;
8
+ }
@@ -0,0 +1,7 @@
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
+ /** Server compilation marker; the application composition root is added by the health slice. */
7
+ export {};
@@ -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,8 @@
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 requiresApproval(policy, requested) {
7
+ return policy !== 'never' && requested;
8
+ }
@@ -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
+ }