@hellolynk-ai/codex-plugin 0.0.0-stage → 0.2.100
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/.agents/plugins/marketplace.json +20 -0
- package/LICENSE +92 -0
- package/README.md +101 -2
- package/bin/hellolynk-client-setup.mjs +2748 -0
- package/bin/hellolynk-mcp-config-shape.mjs +23 -0
- package/hellolynk-client-pack-manifest.json +619 -0
- package/hellolynk-client-pack-pin.json +4 -0
- package/package.json +58 -4
- package/plugins/hellolynk-codex/.app.json +3 -0
- package/plugins/hellolynk-codex/.codex-plugin/plugin.json +38 -0
- package/plugins/hellolynk-codex/.mcp.json +19 -0
- package/plugins/hellolynk-codex/assets/hellolynk-mark.png +0 -0
- package/plugins/hellolynk-codex/package.json +11 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-account.mjs +180 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-core.mjs +770 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-profile.mjs +191 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-auth.mjs +347 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-protected-store.mjs +2721 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-local-process-lock.mjs +157 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-neutral-host-installation-store.mjs +765 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-platform-credential-store.mjs +773 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-runtime-safety.cjs +471 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
- package/plugins/hellolynk-codex/runtime/connect-activate.mjs +837 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-agent-configurator.mjs +701 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-account.mjs +180 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-core.mjs +770 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-profile.mjs +191 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-entry.mjs +31 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-runtime.mjs +600 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update.mjs +453 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-contact-request-e2ee.mjs +867 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-device-enrollment.mjs +435 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-direct-text-e2ee-client.mjs +1964 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-direct-text-e2ee.mjs +1207 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host-auth.mjs +347 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host-protected-store.mjs +2721 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host-transport-client.mjs +946 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host.mjs +2374 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-human-confirmation-ledger.mjs +256 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-human-relationship-controller.mjs +1234 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-inbox-read-state.mjs +11 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-indexed-message-store.mjs +1558 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-instance-conversation.mjs +185 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-contact-requests.mjs +1733 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-contacts.mjs +1228 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-migration.mjs +766 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-protection.mjs +747 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-identity-trust.mjs +274 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-messaging.mjs +3043 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-peer-transport.mjs +103 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-process-lock.mjs +157 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-model-inbox.mjs +230 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-model-notices.cjs +60 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-addressing-tools.cjs +231 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-conversations.mjs +280 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-human-confirmation-broker.mjs +1036 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-inbox-runtime.mjs +553 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-notice.mjs +50 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-reply-routes.mjs +96 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-worker-lease.mjs +84 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-assembly.mjs +955 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-installation-store.mjs +765 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-launcher.mjs +665 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-process.mjs +4374 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-store-adapters.mjs +548 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-workbench-read-model.mjs +1365 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-platform-credential-store.mjs +773 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-relay-client-cli.mjs +232 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-release-retention.mjs +277 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-runtime-safety.cjs +471 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-action-grants.mjs +457 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-connection.mjs +234 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-bootstrap.mjs +64 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-entry.mjs +70 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-loader.mjs +156 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-manifest.json +358 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-pin.json +4 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/configure-agent.ps1 +281 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/credential-store.ps1 +844 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-human-confirmation-ledger.ps1 +155 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-local-data-key.ps1 +300 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-native-human-confirmation-broker.ps1 +216 -0
- package/plugins/hellolynk-codex/runtime/workbench/candidate-bootstrap.mjs +54 -0
- package/plugins/hellolynk-codex/runtime/workbench/contract.mjs +1314 -0
- package/plugins/hellolynk-codex/runtime/workbench/hellolynk-workbench-build.json +215 -0
- package/plugins/hellolynk-codex/runtime/workbench/host-capabilities.mjs +80 -0
- package/plugins/hellolynk-codex/runtime/workbench/i18n.mjs +1127 -0
- package/plugins/hellolynk-codex/runtime/workbench/loopback-surface.mjs +740 -0
- package/plugins/hellolynk-codex/runtime/workbench/mcp-app-bundle.mjs +97 -0
- package/plugins/hellolynk-codex/runtime/workbench/service.mjs +485 -0
- package/plugins/hellolynk-codex/runtime/workbench/state.mjs +421 -0
- package/plugins/hellolynk-codex/runtime/workbench/transports.mjs +171 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/app.mjs +6120 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/connection-form.mjs +155 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/hellolynk-mark.png +0 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/http-transport.mjs +247 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/arrow-left.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/close.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/contacts.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/inbox.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/refresh.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/search.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/security.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/settings.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/index.html +139 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/loopback-entry.mjs +26 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/mcp-app-client.mjs +151 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/mcp-app-entry.mjs +452 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/styles.css +686 -0
- package/plugins/hellolynk-codex/server/codex-neutral-production.mjs +520 -0
- package/plugins/hellolynk-codex/server/mcp-proxy.mjs +306 -0
- package/plugins/hellolynk-codex/server/mcp-server.mjs +1193 -0
- package/plugins/hellolynk-codex/server/native-conversation-directory.mjs +33 -0
- package/plugins/hellolynk-codex/server/native-reply-routes.mjs +32 -0
- package/plugins/hellolynk-codex/server/native-task-client.mjs +382 -0
- package/plugins/hellolynk-codex/server/native-task-runtime.mjs +8 -0
- package/plugins/hellolynk-codex/server/neutral-host-adapter.mjs +649 -0
- package/plugins/hellolynk-codex/server/workbench-runtime.mjs +180 -0
- package/plugins/hellolynk-codex/skills/hellolynk-open/SKILL.md +11 -0
- package/plugins/hellolynk-codex/skills/hellolynk-open/agents/openai.yaml +4 -0
- package/tools/hellolynk-activate-neutral-host-clients.mjs +837 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { validateWorkbenchSnapshot, HELLOLYNK_WORKBENCH_MAX_QUERY_LENGTH } from "./contract.mjs";
|
|
2
|
+
|
|
3
|
+
export const WORKBENCH_PHASES = Object.freeze([
|
|
4
|
+
"loading",
|
|
5
|
+
"empty",
|
|
6
|
+
"ready",
|
|
7
|
+
"error",
|
|
8
|
+
"stale",
|
|
9
|
+
"conflict",
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
export const WORKBENCH_FILTERS = Object.freeze(["all", "unread", "archived"]);
|
|
13
|
+
|
|
14
|
+
export const WORKBENCH_SECTIONS = Object.freeze([
|
|
15
|
+
"inbox",
|
|
16
|
+
"contacts",
|
|
17
|
+
"security",
|
|
18
|
+
"settings",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export const WORKBENCH_FOCUS_TARGETS = Object.freeze([
|
|
22
|
+
"status",
|
|
23
|
+
"navigation",
|
|
24
|
+
"search",
|
|
25
|
+
"thread-list",
|
|
26
|
+
"conversation",
|
|
27
|
+
"section-panel",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const PHASE_SET = new Set(WORKBENCH_PHASES);
|
|
31
|
+
const FILTER_SET = new Set(WORKBENCH_FILTERS);
|
|
32
|
+
const SECTION_SET = new Set(WORKBENCH_SECTIONS);
|
|
33
|
+
const FOCUS_TARGET_SET = new Set(WORKBENCH_FOCUS_TARGETS);
|
|
34
|
+
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{1,198}$/;
|
|
35
|
+
const SAFE_REVISION = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,254}$/;
|
|
36
|
+
const SAFE_ISSUE_CODE = /^[a-z][a-z0-9_.:-]{1,126}$/;
|
|
37
|
+
const MAX_QUERY_LENGTH = HELLOLYNK_WORKBENCH_MAX_QUERY_LENGTH;
|
|
38
|
+
const MAX_ROUTE_HISTORY = 100;
|
|
39
|
+
|
|
40
|
+
export class HelloLynkWorkbenchStateError extends Error {
|
|
41
|
+
constructor(code) {
|
|
42
|
+
super(code);
|
|
43
|
+
this.name = "HelloLynkWorkbenchStateError";
|
|
44
|
+
this.code = code;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createWorkbenchViewState(options = {}) {
|
|
49
|
+
const input = record(options, "workbench_state_options_invalid");
|
|
50
|
+
const filter = input.filter == null ? "all" : oneOf(input.filter, FILTER_SET, "workbench_state_filter_invalid");
|
|
51
|
+
const query = input.query == null ? "" : searchQuery(input.query);
|
|
52
|
+
const selectedThreadId = input.selectedThreadId == null
|
|
53
|
+
? null
|
|
54
|
+
: safeId(input.selectedThreadId, "workbench_state_thread_id_invalid");
|
|
55
|
+
const section = input.section == null
|
|
56
|
+
? "inbox"
|
|
57
|
+
: oneOf(input.section, SECTION_SET, "workbench_state_section_invalid");
|
|
58
|
+
const focusTarget = input.focusTarget == null
|
|
59
|
+
? "status"
|
|
60
|
+
: oneOf(input.focusTarget, FOCUS_TARGET_SET, "workbench_state_focus_target_invalid");
|
|
61
|
+
|
|
62
|
+
if (selectedThreadId != null && section !== "inbox") {
|
|
63
|
+
fail("workbench_state_selection_section_invalid");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const initialRoute = { section, selectedThreadId };
|
|
67
|
+
return deepFreeze({
|
|
68
|
+
phase: "loading",
|
|
69
|
+
filter,
|
|
70
|
+
query,
|
|
71
|
+
selectedThreadId,
|
|
72
|
+
section,
|
|
73
|
+
focusTarget,
|
|
74
|
+
failure: null,
|
|
75
|
+
staleness: null,
|
|
76
|
+
conflict: null,
|
|
77
|
+
snapshotMeta: null,
|
|
78
|
+
knownThreadIds: [],
|
|
79
|
+
routeHistory: {
|
|
80
|
+
entries: [initialRoute],
|
|
81
|
+
index: 0,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function reduceWorkbenchViewState(state, event) {
|
|
87
|
+
const current = viewState(state);
|
|
88
|
+
const input = record(event, "workbench_state_event_invalid");
|
|
89
|
+
|
|
90
|
+
switch (input.type) {
|
|
91
|
+
case "LOAD":
|
|
92
|
+
return updateState(current, {
|
|
93
|
+
phase: "loading",
|
|
94
|
+
focusTarget: "status",
|
|
95
|
+
failure: null,
|
|
96
|
+
staleness: null,
|
|
97
|
+
conflict: null,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
case "LOADED":
|
|
101
|
+
return loadedState(current, input.snapshot);
|
|
102
|
+
|
|
103
|
+
case "FAILED":
|
|
104
|
+
return updateState(current, {
|
|
105
|
+
phase: "error",
|
|
106
|
+
focusTarget: "status",
|
|
107
|
+
failure: {
|
|
108
|
+
code: issueCode(input.code ?? input.error?.code, "workbench_load_failed"),
|
|
109
|
+
retryable: input.retryable === true || input.error?.retryable === true,
|
|
110
|
+
...(input.source === "notification" ? { source: "notification" } : {}),
|
|
111
|
+
},
|
|
112
|
+
staleness: null,
|
|
113
|
+
conflict: null,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
case "STALE":
|
|
117
|
+
return updateState(current, {
|
|
118
|
+
phase: "stale",
|
|
119
|
+
focusTarget: "status",
|
|
120
|
+
failure: null,
|
|
121
|
+
staleness: {
|
|
122
|
+
code: issueCode(input.code, "workbench_snapshot_stale"),
|
|
123
|
+
observedRevision: optionalRevision(input.observedRevision),
|
|
124
|
+
},
|
|
125
|
+
conflict: null,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
case "CONFLICT":
|
|
129
|
+
return updateState(current, {
|
|
130
|
+
phase: "conflict",
|
|
131
|
+
focusTarget: "status",
|
|
132
|
+
failure: null,
|
|
133
|
+
staleness: null,
|
|
134
|
+
conflict: {
|
|
135
|
+
code: issueCode(input.code, "workbench_revision_conflict"),
|
|
136
|
+
expectedRevision: optionalRevision(input.expectedRevision),
|
|
137
|
+
currentRevision: optionalRevision(input.currentRevision),
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
case "SET_FILTER":
|
|
142
|
+
return updateState(current, {
|
|
143
|
+
filter: oneOf(input.filter, FILTER_SET, "workbench_state_filter_invalid"),
|
|
144
|
+
focusTarget: "thread-list",
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
case "SET_QUERY":
|
|
148
|
+
return updateState(current, {
|
|
149
|
+
query: searchQuery(input.query),
|
|
150
|
+
focusTarget: "search",
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
case "SELECT_THREAD": {
|
|
154
|
+
const selectedThreadId = safeId(input.threadId, "workbench_state_thread_id_invalid");
|
|
155
|
+
return navigateTo(current, {
|
|
156
|
+
section: "inbox",
|
|
157
|
+
selectedThreadId,
|
|
158
|
+
}, "conversation");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
case "SET_SECTION": {
|
|
162
|
+
const section = oneOf(input.section, SECTION_SET, "workbench_state_section_invalid");
|
|
163
|
+
const route = {
|
|
164
|
+
section,
|
|
165
|
+
selectedThreadId: section === "inbox" ? current.selectedThreadId : null,
|
|
166
|
+
};
|
|
167
|
+
return navigateTo(current, route, focusForRoute(route));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
case "CLEAR_SELECTION":
|
|
171
|
+
return navigateTo(current, {
|
|
172
|
+
section: current.section,
|
|
173
|
+
selectedThreadId: null,
|
|
174
|
+
}, current.section === "inbox" ? "thread-list" : "section-panel");
|
|
175
|
+
|
|
176
|
+
case "NAVIGATE_BACK":
|
|
177
|
+
return moveInHistory(current, -1);
|
|
178
|
+
|
|
179
|
+
case "NAVIGATE_FORWARD":
|
|
180
|
+
return moveInHistory(current, 1);
|
|
181
|
+
|
|
182
|
+
case "SET_FOCUS":
|
|
183
|
+
return updateState(current, {
|
|
184
|
+
focusTarget: oneOf(input.focusTarget, FOCUS_TARGET_SET, "workbench_state_focus_target_invalid"),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
default:
|
|
188
|
+
fail("workbench_state_event_unsupported");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function selectVisibleThreads(state, snapshot) {
|
|
193
|
+
const current = viewState(state);
|
|
194
|
+
const validated = validateWorkbenchSnapshot(snapshot);
|
|
195
|
+
if (validated.source.mode === "neutral-host") {
|
|
196
|
+
return Object.freeze(validated.threads.items.filter(
|
|
197
|
+
(thread) => matchesConversationFilter(thread, current.filter),
|
|
198
|
+
));
|
|
199
|
+
}
|
|
200
|
+
const tokens = normalizedSearch(current.query).split(/\s+/u).filter(Boolean);
|
|
201
|
+
const messageTextByThread = new Map();
|
|
202
|
+
for (const message of validated.messages.items) {
|
|
203
|
+
const existing = messageTextByThread.get(message.threadId) ?? [];
|
|
204
|
+
existing.push(message.text);
|
|
205
|
+
messageTextByThread.set(message.threadId, existing);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const visible = validated.threads.items.filter((thread) => {
|
|
209
|
+
if (!matchesConversationFilter(thread, current.filter)) return false;
|
|
210
|
+
if (tokens.length === 0) return true;
|
|
211
|
+
const haystack = normalizedSearch([
|
|
212
|
+
thread.displayName,
|
|
213
|
+
thread.peerHelloLynkId,
|
|
214
|
+
thread.delegateLabel ?? "",
|
|
215
|
+
thread.lastMessage?.text ?? "",
|
|
216
|
+
...(messageTextByThread.get(thread.threadId) ?? []),
|
|
217
|
+
].join("\n"));
|
|
218
|
+
return tokens.every((token) => haystack.includes(token));
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
return Object.freeze(visible);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// These selectors only derive presentation data from the authoritative snapshot.
|
|
225
|
+
// They deliberately do not extend the wire contract or create another local store.
|
|
226
|
+
export function deriveWorkbenchConversations(snapshot) {
|
|
227
|
+
const validated = validateWorkbenchSnapshot(snapshot);
|
|
228
|
+
// The Host computes this from the last five messages in each conversation.
|
|
229
|
+
// Old outbox failures remain available for history without resurfacing the
|
|
230
|
+
// list badge while searching or paging through older messages.
|
|
231
|
+
return Object.freeze(validated.threads.items.map((thread) => Object.freeze({
|
|
232
|
+
...thread,
|
|
233
|
+
failedCount: thread.recentSendFailureCount ?? 0,
|
|
234
|
+
hasSendFailure: (thread.recentSendFailureCount ?? 0) > 0,
|
|
235
|
+
})));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function deriveWorkbenchContacts(snapshot) {
|
|
239
|
+
const validated = validateWorkbenchSnapshot(snapshot);
|
|
240
|
+
return Object.freeze(validated.contacts.map((contact) => {
|
|
241
|
+
const relationship = validated.relationships.find(
|
|
242
|
+
(item) => item.otherHelloLynkId === contact.hellolynkId,
|
|
243
|
+
) ?? null;
|
|
244
|
+
const conversations = validated.threads.items.filter((thread) => (
|
|
245
|
+
thread.contactId === contact.contactId
|
|
246
|
+
|| (contact.principalId != null && thread.peerPrincipalId === contact.principalId)
|
|
247
|
+
|| thread.peerHelloLynkId === contact.hellolynkId
|
|
248
|
+
));
|
|
249
|
+
return Object.freeze({
|
|
250
|
+
...contact,
|
|
251
|
+
relationship,
|
|
252
|
+
conversations: Object.freeze(conversations),
|
|
253
|
+
instances: Object.freeze(contact.instances.map((instance) => Object.freeze({
|
|
254
|
+
...instance,
|
|
255
|
+
conversation: conversations.find((thread) => thread.conversationKind === "instance"
|
|
256
|
+
&& thread.localDelegateId === validated.subject.delegateId
|
|
257
|
+
&& thread.peerDelegateId === instance.delegateId
|
|
258
|
+
&& thread.peerPrincipalId === contact.principalId) ?? null,
|
|
259
|
+
}))),
|
|
260
|
+
});
|
|
261
|
+
}));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function matchesConversationFilter(thread, filter) {
|
|
265
|
+
if (filter === "archived") return thread.archived === true;
|
|
266
|
+
if (thread.archived === true) return false;
|
|
267
|
+
if (filter === "unread") return thread.unreadCount > 0;
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function loadedState(state, snapshot) {
|
|
272
|
+
const validated = validateWorkbenchSnapshot(snapshot);
|
|
273
|
+
const knownThreadIds = validated.threads.items.map((thread) => thread.threadId);
|
|
274
|
+
const selectionIsDefinitelyMissing = state.selectedThreadId != null
|
|
275
|
+
&& validated.threads.nextCursor == null
|
|
276
|
+
&& !knownThreadIds.includes(state.selectedThreadId);
|
|
277
|
+
const selectedThreadId = selectionIsDefinitelyMissing ? null : state.selectedThreadId;
|
|
278
|
+
const routeHistory = selectionIsDefinitelyMissing
|
|
279
|
+
? replaceCurrentRoute(state.routeHistory, { section: state.section, selectedThreadId: null })
|
|
280
|
+
: state.routeHistory;
|
|
281
|
+
const phase = knownThreadIds.length === 0 && validated.threads.nextCursor == null ? "empty" : "ready";
|
|
282
|
+
|
|
283
|
+
return updateState(state, {
|
|
284
|
+
phase,
|
|
285
|
+
selectedThreadId,
|
|
286
|
+
focusTarget: phase === "empty" && state.section === "inbox"
|
|
287
|
+
? "status"
|
|
288
|
+
: focusForRoute({ section: state.section, selectedThreadId }),
|
|
289
|
+
failure: null,
|
|
290
|
+
staleness: null,
|
|
291
|
+
conflict: null,
|
|
292
|
+
knownThreadIds,
|
|
293
|
+
routeHistory,
|
|
294
|
+
snapshotMeta: {
|
|
295
|
+
hostGeneration: validated.hostGeneration,
|
|
296
|
+
revision: validated.revision,
|
|
297
|
+
changeCursor: validated.changeCursor,
|
|
298
|
+
generatedAt: validated.generatedAt,
|
|
299
|
+
source: validated.source,
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function navigateTo(state, route, focusTarget) {
|
|
305
|
+
const routeHistory = pushRoute(state.routeHistory, route);
|
|
306
|
+
return updateState(state, {
|
|
307
|
+
section: route.section,
|
|
308
|
+
selectedThreadId: route.selectedThreadId,
|
|
309
|
+
focusTarget,
|
|
310
|
+
routeHistory,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function moveInHistory(state, delta) {
|
|
315
|
+
const nextIndex = state.routeHistory.index + delta;
|
|
316
|
+
if (nextIndex < 0 || nextIndex >= state.routeHistory.entries.length) return state;
|
|
317
|
+
const route = state.routeHistory.entries[nextIndex];
|
|
318
|
+
return updateState(state, {
|
|
319
|
+
section: route.section,
|
|
320
|
+
selectedThreadId: route.selectedThreadId,
|
|
321
|
+
focusTarget: focusForRoute(route),
|
|
322
|
+
routeHistory: {
|
|
323
|
+
entries: state.routeHistory.entries,
|
|
324
|
+
index: nextIndex,
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function pushRoute(history, route) {
|
|
330
|
+
const current = history.entries[history.index];
|
|
331
|
+
if (sameRoute(current, route)) return history;
|
|
332
|
+
|
|
333
|
+
let entries = [...history.entries.slice(0, history.index + 1), route];
|
|
334
|
+
if (entries.length > MAX_ROUTE_HISTORY) entries = entries.slice(entries.length - MAX_ROUTE_HISTORY);
|
|
335
|
+
return {
|
|
336
|
+
entries,
|
|
337
|
+
index: entries.length - 1,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function replaceCurrentRoute(history, route) {
|
|
342
|
+
const entries = [...history.entries];
|
|
343
|
+
entries[history.index] = route;
|
|
344
|
+
return { entries, index: history.index };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function sameRoute(left, right) {
|
|
348
|
+
return left.section === right.section && left.selectedThreadId === right.selectedThreadId;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function focusForRoute(route) {
|
|
352
|
+
if (route.selectedThreadId != null) return "conversation";
|
|
353
|
+
return route.section === "inbox" ? "thread-list" : "section-panel";
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function updateState(state, patch) {
|
|
357
|
+
return deepFreeze({ ...state, ...patch });
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function viewState(value) {
|
|
361
|
+
const state = record(value, "workbench_state_invalid");
|
|
362
|
+
if (!PHASE_SET.has(state.phase)
|
|
363
|
+
|| !FILTER_SET.has(state.filter)
|
|
364
|
+
|| !SECTION_SET.has(state.section)
|
|
365
|
+
|| !FOCUS_TARGET_SET.has(state.focusTarget)
|
|
366
|
+
|| !Array.isArray(state.routeHistory?.entries)
|
|
367
|
+
|| !Number.isSafeInteger(state.routeHistory?.index)
|
|
368
|
+
|| state.routeHistory.index < 0
|
|
369
|
+
|| state.routeHistory.index >= state.routeHistory.entries.length) {
|
|
370
|
+
fail("workbench_state_invalid");
|
|
371
|
+
}
|
|
372
|
+
return state;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function searchQuery(value) {
|
|
376
|
+
if (typeof value !== "string" || value.length > MAX_QUERY_LENGTH || value.includes("\u0000")) {
|
|
377
|
+
fail("workbench_state_query_invalid");
|
|
378
|
+
}
|
|
379
|
+
return value;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function normalizedSearch(value) {
|
|
383
|
+
return String(value).normalize("NFKC").trim().toLowerCase();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function safeId(value, code) {
|
|
387
|
+
if (typeof value !== "string" || !SAFE_ID.test(value)) fail(code);
|
|
388
|
+
return value;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function optionalRevision(value) {
|
|
392
|
+
if (value == null) return null;
|
|
393
|
+
if (typeof value !== "string" || !SAFE_REVISION.test(value)) fail("workbench_state_revision_invalid");
|
|
394
|
+
return value;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function issueCode(value, fallback) {
|
|
398
|
+
if (value == null) return fallback;
|
|
399
|
+
if (typeof value !== "string" || !SAFE_ISSUE_CODE.test(value)) fail("workbench_state_issue_code_invalid");
|
|
400
|
+
return value;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function oneOf(value, allowed, code) {
|
|
404
|
+
if (!allowed.has(value)) fail(code);
|
|
405
|
+
return value;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function record(value, code) {
|
|
409
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) fail(code);
|
|
410
|
+
return value;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function deepFreeze(value) {
|
|
414
|
+
if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
|
|
415
|
+
for (const nested of Object.values(value)) deepFreeze(nested);
|
|
416
|
+
return Object.freeze(value);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function fail(code) {
|
|
420
|
+
throw new HelloLynkWorkbenchStateError(code);
|
|
421
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HELLOLYNK_WORKBENCH_COMMAND_TYPES,
|
|
3
|
+
HELLOLYNK_WORKBENCH_RESULT_CONTRACT,
|
|
4
|
+
HELLOLYNK_WORKBENCH_SCHEMA_VERSION,
|
|
5
|
+
validateWorkbenchCommand,
|
|
6
|
+
validateWorkbenchCommandResult,
|
|
7
|
+
validateWorkbenchReadRequest,
|
|
8
|
+
validateWorkbenchSnapshot,
|
|
9
|
+
} from "./contract.mjs";
|
|
10
|
+
|
|
11
|
+
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{1,198}$/;
|
|
12
|
+
|
|
13
|
+
export class HelloLynkWorkbenchTransportError extends Error {
|
|
14
|
+
constructor(code) {
|
|
15
|
+
super(code);
|
|
16
|
+
this.name = "HelloLynkWorkbenchTransportError";
|
|
17
|
+
this.code = code;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createLoopbackHttpWorkbenchTransport(options = {}) {
|
|
22
|
+
return createValidatedTransport("loopback-http", options);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createMcpAppRpcWorkbenchTransport(options = {}) {
|
|
26
|
+
return createValidatedTransport("mcp-app-rpc", options);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createSyntheticMemoryWorkbenchTransport(options = {}) {
|
|
30
|
+
if (options.explicitSynthetic !== true) fail("workbench_synthetic_transport_requires_explicit_injection");
|
|
31
|
+
return createValidatedTransport("synthetic-memory", options);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createValidatedTransport(transport, options) {
|
|
35
|
+
const readSnapshot = requiredFunction(options.readSnapshot, "workbench_transport_read_snapshot_required");
|
|
36
|
+
const synthetic = transport === "synthetic-memory";
|
|
37
|
+
const executeCommand = synthetic ? null : requiredFunction(options.executeCommand, "workbench_transport_execute_command_required");
|
|
38
|
+
const issueActionGrant = synthetic ? null : requiredFunction(options.issueActionGrant, "workbench_transport_action_grant_issuer_required");
|
|
39
|
+
const source = Object.freeze({
|
|
40
|
+
mode: transport === "synthetic-memory" ? "synthetic-preview" : "neutral-host",
|
|
41
|
+
transport,
|
|
42
|
+
clientId: safeId(options.clientId, "workbench_transport_client_id_invalid"),
|
|
43
|
+
clientLabel: boundedText(options.clientLabel, 80, "workbench_transport_client_label_invalid"),
|
|
44
|
+
});
|
|
45
|
+
let snapshotRequiredAfterUnknown = false;
|
|
46
|
+
|
|
47
|
+
return Object.freeze({
|
|
48
|
+
kind: transport,
|
|
49
|
+
source,
|
|
50
|
+
actionGrantMode: synthetic ? "none" : "host-single-use-v1",
|
|
51
|
+
|
|
52
|
+
async readSnapshot(input = {}) {
|
|
53
|
+
const request = validateWorkbenchReadRequest(input);
|
|
54
|
+
const snapshot = validateWorkbenchSnapshot(await readSnapshot(request, { source }));
|
|
55
|
+
assertSnapshotSource(snapshot.source, source);
|
|
56
|
+
snapshotRequiredAfterUnknown = false;
|
|
57
|
+
return snapshot;
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
async executeCommand(value) {
|
|
61
|
+
const command = validateWorkbenchCommand(value);
|
|
62
|
+
if (synthetic) return knownFailureResult(command.requestId, "workbench_transport_commands_not_enabled");
|
|
63
|
+
const statusRead = command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.getSendStatus;
|
|
64
|
+
if (snapshotRequiredAfterUnknown && !statusRead) {
|
|
65
|
+
return knownFailureResult(command.requestId, "workbench_transport_snapshot_required");
|
|
66
|
+
}
|
|
67
|
+
let actionGrant;
|
|
68
|
+
if (!statusRead) {
|
|
69
|
+
try {
|
|
70
|
+
const issued = await issueActionGrant(command, { source });
|
|
71
|
+
actionGrant = privateActionGrant(issued?.actionGrant);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
return knownFailureResult(command.requestId, publicPreMutationCode(error));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const result = validateWorkbenchCommandResult(
|
|
78
|
+
await executeCommand(command, statusRead ? { source } : { source, actionGrant }),
|
|
79
|
+
command.command,
|
|
80
|
+
);
|
|
81
|
+
if (result.requestId !== command.requestId) throw new Error("workbench_transport_request_id_mismatch");
|
|
82
|
+
if (!statusRead && !result.ok && result.error.outcome === "unknown") snapshotRequiredAfterUnknown = true;
|
|
83
|
+
return result;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (error?.outcome === "known-failed") {
|
|
86
|
+
return knownFailureResult(command.requestId, publicPreMutationCode(error));
|
|
87
|
+
}
|
|
88
|
+
if (!statusRead) snapshotRequiredAfterUnknown = true;
|
|
89
|
+
return unknownOutcomeResult(command.requestId);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function privateActionGrant(value) {
|
|
96
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{31,511}$/.test(value)) {
|
|
97
|
+
fail("workbench_transport_action_grant_invalid");
|
|
98
|
+
}
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function publicPreMutationCode(error) {
|
|
103
|
+
const code = String(error?.code || "workbench_transport_action_grant_failed");
|
|
104
|
+
return /^(?:workbench_transport_[a-z0-9_]{1,100}|host_app_action_grant_[a-z0-9_]{1,100})$/.test(code)
|
|
105
|
+
? code
|
|
106
|
+
: "workbench_transport_action_grant_failed";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function knownFailureResult(requestId, code) {
|
|
110
|
+
return validateWorkbenchCommandResult({
|
|
111
|
+
contractId: HELLOLYNK_WORKBENCH_RESULT_CONTRACT,
|
|
112
|
+
schemaVersion: HELLOLYNK_WORKBENCH_SCHEMA_VERSION,
|
|
113
|
+
ok: false,
|
|
114
|
+
requestId,
|
|
115
|
+
revision: null,
|
|
116
|
+
changeCursor: null,
|
|
117
|
+
error: {
|
|
118
|
+
code,
|
|
119
|
+
retryable: false,
|
|
120
|
+
outcome: "known-failed",
|
|
121
|
+
retryAfterMs: null,
|
|
122
|
+
currentRevision: null,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function unknownOutcomeResult(requestId) {
|
|
128
|
+
return validateWorkbenchCommandResult({
|
|
129
|
+
contractId: HELLOLYNK_WORKBENCH_RESULT_CONTRACT,
|
|
130
|
+
schemaVersion: HELLOLYNK_WORKBENCH_SCHEMA_VERSION,
|
|
131
|
+
ok: false,
|
|
132
|
+
requestId,
|
|
133
|
+
revision: null,
|
|
134
|
+
changeCursor: null,
|
|
135
|
+
error: {
|
|
136
|
+
code: "workbench_transport_outcome_unknown",
|
|
137
|
+
retryable: false,
|
|
138
|
+
outcome: "unknown",
|
|
139
|
+
retryAfterMs: null,
|
|
140
|
+
currentRevision: null,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function assertSnapshotSource(actual, expected) {
|
|
146
|
+
if (actual.mode !== expected.mode
|
|
147
|
+
|| actual.transport !== expected.transport
|
|
148
|
+
|| actual.clientId !== expected.clientId
|
|
149
|
+
|| actual.clientLabel !== expected.clientLabel) {
|
|
150
|
+
fail("workbench_transport_snapshot_source_mismatch");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function requiredFunction(value, code) {
|
|
155
|
+
if (typeof value !== "function") fail(code);
|
|
156
|
+
return value;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function safeId(value, code) {
|
|
160
|
+
if (typeof value !== "string" || !SAFE_ID.test(value)) fail(code);
|
|
161
|
+
return value;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function boundedText(value, maximum, code) {
|
|
165
|
+
if (typeof value !== "string" || !value.trim() || value.length > maximum || value.includes("\u0000")) fail(code);
|
|
166
|
+
return value;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function fail(code) {
|
|
170
|
+
throw new HelloLynkWorkbenchTransportError(code);
|
|
171
|
+
}
|