gestalt-mobile 0.35.4 → 0.36.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 (49) hide show
  1. package/README.md +60 -3
  2. package/dist/client/assets/index-9XMv2BvD.js +38 -0
  3. package/dist/client/assets/{index-CPQBJDsg.css → index-Bxv5Z2w4.css} +1 -1
  4. package/dist/client/index.html +2 -2
  5. package/dist/server/client/features/chat/activity-summary.js +71 -0
  6. package/dist/server/client/features/chat/chat-projection.js +722 -0
  7. package/dist/server/client/features/chat/message-store.js +30 -0
  8. package/dist/server/server/app.js +1 -1
  9. package/dist/server/server/cli.js +8 -0
  10. package/dist/server/server/composition.contracts.js +191 -8
  11. package/dist/server/server/composition.js +266 -56
  12. package/dist/server/server/features/autopilot/application/policy.js +2 -2
  13. package/dist/server/server/features/autopilot/register-routes.js +2 -2
  14. package/dist/server/server/features/autopilot/toggle/endpoint.js +5 -1
  15. package/dist/server/server/features/catalog/get-bootstrap/use-case.js +5 -3
  16. package/dist/server/server/features/plans/application/parse-supervised-plan.js +33 -8
  17. package/dist/server/server/features/sessions/get-session/endpoint.js +1 -1
  18. package/dist/server/server/features/sessions/list-models/endpoint.js +16 -0
  19. package/dist/server/server/features/sessions/list-recent-threads/endpoint.js +8 -3
  20. package/dist/server/server/features/sessions/list-sessions/endpoint.js +1 -1
  21. package/dist/server/server/features/sessions/model/relay-session.js +2 -0
  22. package/dist/server/server/features/sessions/promote-recent-thread/endpoint.js +6 -2
  23. package/dist/server/server/features/sessions/promote-recent-thread/use-case.js +7 -2
  24. package/dist/server/server/features/sessions/register-routes.js +8 -3
  25. package/dist/server/server/features/sessions/respond-interaction/endpoint.js +1 -1
  26. package/dist/server/server/features/sessions/select-model/endpoint.js +1 -1
  27. package/dist/server/server/features/sessions/start-session/request.js +1 -0
  28. package/dist/server/server/features/sessions/start-session/use-case.js +21 -7
  29. package/dist/server/server/features/skills/list-available/endpoint.js +4 -2
  30. package/dist/server/server/platform/catalog/provider-model-catalog.js +15 -0
  31. package/dist/server/server/platform/kimi/kimi-activity-facts.js +88 -0
  32. package/dist/server/server/platform/kimi/kimi-errors.js +30 -0
  33. package/dist/server/server/platform/kimi/kimi-model-catalog.js +63 -0
  34. package/dist/server/server/platform/kimi/kimi-normalizer.js +196 -0
  35. package/dist/server/server/platform/kimi/kimi-recent-session-lister.js +68 -0
  36. package/dist/server/server/platform/kimi/kimi-server-request.js +143 -0
  37. package/dist/server/server/platform/kimi/kimi-session-runtime.js +653 -0
  38. package/dist/server/server/platform/kimi/kimi-share-dir.js +65 -0
  39. package/dist/server/server/platform/kimi/kimi-skill-catalog.js +79 -0
  40. package/dist/server/server/platform/kimi/kimi-skills-materializer.js +41 -0
  41. package/dist/server/server/platform/kimi/kimi-web-client.js +63 -0
  42. package/dist/server/server/platform/kimi/kimi-web-server-manager.js +215 -0
  43. package/dist/server/server/platform/kimi/kimi-ws-client.js +154 -0
  44. package/dist/server/server/platform/persistence/migrate.js +2 -0
  45. package/dist/server/server/platform/persistence/sqlite-session-repository.js +3 -2
  46. package/dist/server/server/platform/skills/cached-skill-catalog.js +0 -0
  47. package/dist/server/shared/contracts/llm-provider.js +6 -0
  48. package/package.json +4 -3
  49. package/dist/client/assets/index-SZM7lpix.js +0 -38
@@ -0,0 +1,196 @@
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
+ /**
7
+ * Maps kimi web session events onto the relay's provider-neutral event types.
8
+ * The relay's turn lifecycle is prompt-based (`prompt.completed`/`aborted`),
9
+ * because a gestalt turn id is the kimi prompt id; `turn.started` exists only
10
+ * to bind kimi per-agent turn numbers to prompt ids. Unknown future event
11
+ * types deliberately become `null`, mirroring the codex normalizer.
12
+ */
13
+ export function normalizeKimiEvent(sessionId, sequence, occurredAt, event, context = {
14
+ resolveTurnId: () => null,
15
+ isChildAgent: () => false,
16
+ }) {
17
+ const payload = record(event.payload);
18
+ if (!payload || typeof payload.type !== 'string')
19
+ return null;
20
+ switch (payload.type) {
21
+ case 'assistant.delta': {
22
+ const agentId = safeId(payload.agentId);
23
+ const turnNumber = safeTurn(payload.turnId);
24
+ const delta = bounded(payload.delta, 64_000);
25
+ if (!agentId || turnNumber === null || delta === null)
26
+ return null;
27
+ if (context.isChildAgent(agentId))
28
+ return null;
29
+ const turnId = context.resolveTurnId(agentId, turnNumber) ?? `kimi:${agentId}:${turnNumber}`;
30
+ return {
31
+ sessionId,
32
+ sequence,
33
+ occurredAt,
34
+ type: 'agentMessageDelta',
35
+ payload: { text: delta, itemId: `kimi:${agentId}:${turnNumber}`, turnId },
36
+ };
37
+ }
38
+ case 'tool.call.started': {
39
+ const toolCallId = safeId(payload.toolCallId);
40
+ const agentId = safeId(payload.agentId) ?? 'main';
41
+ const turnNumber = safeTurn(payload.turnId);
42
+ if (!toolCallId || turnNumber === null)
43
+ return null;
44
+ const display = record(payload.display);
45
+ const kind = typeof display?.kind === 'string' ? display.kind : 'generic';
46
+ const name = safeId(payload.name);
47
+ const description = bounded(payload.description, 500);
48
+ const detail = (typeof display?.command === 'string' ? bounded(display.command, 500) : null) ??
49
+ (typeof display?.path === 'string' ? bounded(display.path, 500) : null) ??
50
+ description ??
51
+ name ??
52
+ kind;
53
+ const label = kind === 'command' || kind === 'bash'
54
+ ? 'Command'
55
+ : kind === 'file_io'
56
+ ? 'File'
57
+ : `Tool · ${name ?? kind}`;
58
+ const owner = ownership(agentId, turnNumber, context);
59
+ return {
60
+ sessionId,
61
+ sequence,
62
+ occurredAt,
63
+ type: 'activity.updated',
64
+ payload: { id: toolCallId, label, detail, ...owner },
65
+ };
66
+ }
67
+ case 'tool.result': {
68
+ const toolCallId = safeId(payload.toolCallId);
69
+ const agentId = safeId(payload.agentId) ?? 'main';
70
+ const turnNumber = safeTurn(payload.turnId);
71
+ if (!toolCallId || turnNumber === null)
72
+ return null;
73
+ const isError = payload.isError === true;
74
+ const owner = ownership(agentId, turnNumber, context);
75
+ return {
76
+ sessionId,
77
+ sequence,
78
+ occurredAt,
79
+ type: 'activity.updated',
80
+ payload: {
81
+ id: toolCallId,
82
+ label: isError ? 'Tool error' : 'Tool result',
83
+ detail: bounded(safeJson(payload.output), 400) ?? (isError ? 'failed' : 'done'),
84
+ ...owner,
85
+ },
86
+ };
87
+ }
88
+ case 'subagent.spawned': {
89
+ const subagentId = safeId(payload.subagentId);
90
+ if (!subagentId)
91
+ return null;
92
+ const name = bounded(payload.subagentName, 128);
93
+ const description = bounded(payload.description, 300);
94
+ return {
95
+ sessionId,
96
+ sequence,
97
+ occurredAt,
98
+ type: 'activity.updated',
99
+ payload: {
100
+ id: subagentId,
101
+ label: 'Subagent',
102
+ detail: [name, description].filter(Boolean).join(' · '),
103
+ ...(context.activeTurnId ? { turnId: context.activeTurnId } : {}),
104
+ },
105
+ };
106
+ }
107
+ case 'subagent.completed':
108
+ case 'subagent.failed': {
109
+ const subagentId = safeId(payload.subagentId);
110
+ if (!subagentId)
111
+ return null;
112
+ const failed = payload.type === 'subagent.failed';
113
+ return {
114
+ sessionId,
115
+ sequence,
116
+ occurredAt,
117
+ type: 'activity.updated',
118
+ payload: {
119
+ id: subagentId,
120
+ label: failed ? 'Subagent failed' : 'Subagent completed',
121
+ detail: bounded(payload.resultSummary, 500) ?? '',
122
+ ...(context.activeTurnId ? { turnId: context.activeTurnId } : {}),
123
+ },
124
+ };
125
+ }
126
+ case 'prompt.completed': {
127
+ const agentId = safeId(payload.agentId);
128
+ const promptId = safeId(payload.promptId);
129
+ if (!agentId || !promptId || context.isChildAgent(agentId))
130
+ return null;
131
+ return {
132
+ sessionId,
133
+ sequence,
134
+ occurredAt,
135
+ type: 'turnCompleted',
136
+ payload: {
137
+ turn: {
138
+ id: promptId,
139
+ ...(typeof payload.reason === 'string' ? { status: bounded(payload.reason, 64) } : {}),
140
+ },
141
+ },
142
+ };
143
+ }
144
+ case 'prompt.aborted': {
145
+ const agentId = safeId(payload.agentId);
146
+ const promptId = safeId(payload.promptId);
147
+ if (!agentId || !promptId || context.isChildAgent(agentId))
148
+ return null;
149
+ return {
150
+ sessionId,
151
+ sequence,
152
+ occurredAt,
153
+ type: 'turnInterrupted',
154
+ payload: { turn: { id: promptId } },
155
+ };
156
+ }
157
+ default:
158
+ return null;
159
+ }
160
+ }
161
+ function ownership(agentId, turnNumber, context) {
162
+ if (context.isChildAgent(agentId)) {
163
+ return {
164
+ ...(context.activeTurnId ? { turnId: context.activeTurnId } : {}),
165
+ actorTurnId: `${agentId}:${turnNumber}`,
166
+ };
167
+ }
168
+ const turnId = context.resolveTurnId(agentId, turnNumber);
169
+ return turnId ? { turnId } : context.activeTurnId ? { turnId: context.activeTurnId } : {};
170
+ }
171
+ function record(value) {
172
+ return value && typeof value === 'object' && !Array.isArray(value)
173
+ ? value
174
+ : null;
175
+ }
176
+ function safeId(value) {
177
+ return typeof value === 'string' && value.length > 0 && value.length <= 256 ? value : null;
178
+ }
179
+ function safeTurn(value) {
180
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : null;
181
+ }
182
+ function bounded(value, max) {
183
+ return typeof value === 'string' ? value.slice(0, max) : null;
184
+ }
185
+ function safeJson(value) {
186
+ if (value === undefined || value === null)
187
+ return null;
188
+ if (typeof value === 'string')
189
+ return value.slice(0, 400);
190
+ try {
191
+ return JSON.stringify(value).slice(0, 400);
192
+ }
193
+ catch {
194
+ return null;
195
+ }
196
+ }
@@ -0,0 +1,68 @@
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
+ const LIST_PATH = '/api/v2/sessions?page_size=50&sort=meta.updated_at_desc';
7
+ const LIST_TIMEOUT_MS = 5_000;
8
+ /**
9
+ * Lists recent kimi web sessions across every running gestalt-owned server,
10
+ * for parity with the codex recent-thread lister. Each skill profile owns an
11
+ * isolated `kimi web` process and state directory, so a profile's sessions
12
+ * only exist on that profile's server; only already-running servers are
13
+ * queried, never spawned just to list. kimi timestamps arrive as epoch
14
+ * seconds (or, defensively, epoch millis), normalized to seconds.
15
+ */
16
+ export function createKimiRecentSessionLister(deps) {
17
+ return {
18
+ async list() {
19
+ if (!deps.available || !deps.servers)
20
+ return [];
21
+ const listings = await Promise.all(deps.servers.list().map(listFromServer));
22
+ return listings.flat().sort((a, b) => (b.recencyAt ?? 0) - (a.recencyAt ?? 0));
23
+ },
24
+ };
25
+ }
26
+ async function listFromServer(handle) {
27
+ try {
28
+ const data = (await withTimeout(handle.client.get(LIST_PATH), LIST_TIMEOUT_MS, 'kimi session list timed out'));
29
+ const items = Array.isArray(data.items) ? data.items : [];
30
+ return items.flatMap((item) => {
31
+ if (!item || typeof item !== 'object')
32
+ return [];
33
+ const workspace = item.workspace;
34
+ const cwd = workspace && typeof workspace === 'object'
35
+ ? workspace.cwd
36
+ : undefined;
37
+ if (typeof item.id !== 'string' || typeof cwd !== 'string' || !cwd.startsWith('/'))
38
+ return [];
39
+ const updatedAt = item.meta && typeof item.meta === 'object'
40
+ ? item.meta.updated_at
41
+ : undefined;
42
+ return [
43
+ {
44
+ id: item.id,
45
+ cwd,
46
+ profile: handle.profileKey,
47
+ recencyAt: toEpochSeconds(updatedAt),
48
+ provider: 'kimi',
49
+ },
50
+ ];
51
+ });
52
+ }
53
+ catch {
54
+ return [];
55
+ }
56
+ }
57
+ function toEpochSeconds(value) {
58
+ if (typeof value !== 'number' || !Number.isFinite(value))
59
+ return null;
60
+ // Epoch millis are roughly 1e12+ today; seconds stay below 1e11 for decades.
61
+ return value > 1e11 ? Math.floor(value / 1000) : Math.floor(value);
62
+ }
63
+ function withTimeout(promise, timeoutMs, message) {
64
+ return Promise.race([
65
+ promise,
66
+ new Promise((_resolve, reject) => setTimeout(() => reject(new Error(message)), timeoutMs).unref()),
67
+ ]);
68
+ }
@@ -0,0 +1,143 @@
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
+ function safeId(value) {
7
+ return typeof value === 'string' && value.length > 0 && value.length <= 256 ? value : undefined;
8
+ }
9
+ function bounded(value, max = 1_000) {
10
+ if (typeof value !== 'string')
11
+ return '';
12
+ return value.slice(0, max);
13
+ }
14
+ function displayText(display) {
15
+ if (!display || typeof display !== 'object')
16
+ return '';
17
+ const record = display;
18
+ for (const key of ['command', 'path', 'detail', 'description', 'text']) {
19
+ if (typeof record[key] === 'string')
20
+ return bounded(record[key], 500);
21
+ }
22
+ try {
23
+ return bounded(JSON.stringify(display), 500);
24
+ }
25
+ catch {
26
+ return '';
27
+ }
28
+ }
29
+ /** Command-shaped approvals steer to commandExecution; file mutations to fileChange. */
30
+ export function approvalToServerRequest(id, item) {
31
+ const approvalId = safeId(item.approval_id);
32
+ if (!approvalId)
33
+ return null;
34
+ const toolName = (item.tool_name ?? '').toLowerCase();
35
+ const fileLike = /file|write|edit|patch|str_replace|apply|notebook/.test(toolName);
36
+ const detail = displayText(item.tool_input_display);
37
+ return {
38
+ id,
39
+ method: fileLike ? 'item/fileChange/requestApproval' : 'item/commandExecution/requestApproval',
40
+ params: {
41
+ itemId: approvalId,
42
+ ...(item.tool_name ? { toolName: bounded(item.tool_name, 128) } : {}),
43
+ ...(item.action ? { reason: bounded(item.action, 256) } : {}),
44
+ ...(detail ? { [fileLike ? 'changes' : 'command']: detail } : {}),
45
+ },
46
+ };
47
+ }
48
+ export function questionToServerRequest(id, item) {
49
+ const questionId = safeId(item.question_id);
50
+ if (!questionId)
51
+ return null;
52
+ const questions = (item.questions ?? []).flatMap((question) => {
53
+ if (!safeId(question.id) || typeof question.question !== 'string')
54
+ return [];
55
+ return [
56
+ {
57
+ id: question.id,
58
+ question: bounded(question.question, 1_000),
59
+ ...(question.header ? { header: bounded(question.header, 200) } : {}),
60
+ ...(question.body ? { body: bounded(question.body, 2_000) } : {}),
61
+ ...(question.options && question.options.length
62
+ ? {
63
+ options: question.options.flatMap((option) => safeId(option.id) && typeof option.label === 'string'
64
+ ? [
65
+ {
66
+ id: option.id,
67
+ label: bounded(option.label, 200),
68
+ ...(option.description
69
+ ? { description: bounded(option.description, 500) }
70
+ : {}),
71
+ },
72
+ ]
73
+ : []),
74
+ }
75
+ : {}),
76
+ },
77
+ ];
78
+ });
79
+ return {
80
+ id,
81
+ method: 'item/tool/requestUserInput',
82
+ params: { questions },
83
+ };
84
+ }
85
+ /** Translates a relay approval decision into a kimi approval decision. */
86
+ export function kimiApprovalDecision(value) {
87
+ if (value === 'accept' || value === 'acceptForSession')
88
+ return 'approved';
89
+ if (value === 'decline')
90
+ return 'rejected';
91
+ if (value === 'cancel')
92
+ return 'cancelled';
93
+ return null;
94
+ }
95
+ /**
96
+ * Translates a relay userInput response (`{answers: {<qid>: {answers: string[]}}}`)
97
+ * into kimi's question answers map. An answer matching an option id or label
98
+ * becomes a single choice; anything else becomes free text.
99
+ */
100
+ export function kimiQuestionAnswers(item, value) {
101
+ if (!value || typeof value !== 'object')
102
+ return null;
103
+ const answers = value.answers;
104
+ if (!answers || typeof answers !== 'object' || Array.isArray(answers))
105
+ return null;
106
+ const result = {};
107
+ for (const [questionId, rawAnswer] of Object.entries(answers)) {
108
+ const reply = rawAnswer && typeof rawAnswer === 'object' ? rawAnswer : {};
109
+ const texts = Array.isArray(reply.answers)
110
+ ? reply.answers.filter((entry) => typeof entry === 'string')
111
+ : [];
112
+ if (!texts.length) {
113
+ result[questionId] = { kind: 'skipped' };
114
+ continue;
115
+ }
116
+ const question = (item.questions ?? []).find((candidate) => candidate.id === questionId);
117
+ const option = question?.options?.find((candidate) => candidate.id === texts[0] || candidate.label === texts[0]);
118
+ result[questionId] = option
119
+ ? { kind: 'single', option_id: option.id }
120
+ : { kind: 'other', text: texts.join('\n').slice(0, 4_000) };
121
+ }
122
+ return Object.keys(result).length ? result : null;
123
+ }
124
+ export function decodeApprovalItems(data) {
125
+ const items = data?.items;
126
+ if (!Array.isArray(items))
127
+ return [];
128
+ return items.filter((item) => {
129
+ if (!item || typeof item !== 'object')
130
+ return false;
131
+ return safeId(item.approval_id) !== undefined;
132
+ });
133
+ }
134
+ export function decodeQuestionItems(data) {
135
+ const items = data?.items;
136
+ if (!Array.isArray(items))
137
+ return [];
138
+ return items.filter((item) => {
139
+ if (!item || typeof item !== 'object')
140
+ return false;
141
+ return safeId(item.question_id) !== undefined;
142
+ });
143
+ }