forge-openclaw-plugin 0.2.24 → 0.2.25
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/README.md +13 -0
- package/dist/assets/{board-_C6oMy5w.js → board-VmF4FAfr.js} +3 -3
- package/dist/assets/{board-_C6oMy5w.js.map → board-VmF4FAfr.js.map} +1 -1
- package/dist/assets/index-CFCKDIMH.js +67 -0
- package/dist/assets/index-CFCKDIMH.js.map +1 -0
- package/dist/assets/index-ZPY6U1TU.css +1 -0
- package/dist/assets/{motion-D4sZgCHd.js → motion-DvkU14p-.js} +3 -3
- package/dist/assets/motion-DvkU14p-.js.map +1 -0
- package/dist/assets/{table-BWzTaky1.js → table-DgiPof9E.js} +2 -2
- package/dist/assets/{table-BWzTaky1.js.map → table-DgiPof9E.js.map} +1 -1
- package/dist/assets/{ui-BzK4azQb.js → ui-nYfoC0Gq.js} +2 -2
- package/dist/assets/{ui-BzK4azQb.js.map → ui-nYfoC0Gq.js.map} +1 -1
- package/dist/assets/vendor-D9PTEPSB.js +824 -0
- package/dist/assets/vendor-D9PTEPSB.js.map +1 -0
- package/dist/assets/viz-Cqb6s--o.js +34 -0
- package/dist/assets/viz-Cqb6s--o.js.map +1 -0
- package/dist/index.html +8 -8
- package/dist/openclaw/parity.d.ts +1 -1
- package/dist/openclaw/parity.js +29 -0
- package/dist/openclaw/plugin-entry-shared.d.ts +1 -0
- package/dist/openclaw/plugin-entry-shared.js +7 -4
- package/dist/openclaw/plugin-sdk-types.d.ts +12 -0
- package/dist/openclaw/routes.js +236 -0
- package/dist/openclaw/session-bootstrap.d.ts +78 -0
- package/dist/openclaw/session-bootstrap.js +240 -0
- package/dist/openclaw/tools.js +279 -3
- package/dist/server/app.js +855 -19
- package/dist/server/connectors/box-registry.js +257 -0
- package/dist/server/db.js +2 -0
- package/dist/server/discovery-advertiser.js +114 -0
- package/dist/server/health.js +39 -11
- package/dist/server/index.js +4 -0
- package/dist/server/managers/platform/llm-manager.js +40 -4
- package/dist/server/managers/platform/openai-responses-provider.js +129 -19
- package/dist/server/movement.js +2935 -0
- package/dist/server/openapi.js +628 -5
- package/dist/server/psyche-types.js +15 -1
- package/dist/server/questionnaire-flow.js +552 -0
- package/dist/server/questionnaire-seeds.js +853 -0
- package/dist/server/questionnaire-types.js +340 -0
- package/dist/server/repositories/ai-connectors.js +944 -0
- package/dist/server/repositories/ai-processors.js +547 -0
- package/dist/server/repositories/entity-ownership.js +9 -1
- package/dist/server/repositories/habits.js +69 -5
- package/dist/server/repositories/model-settings.js +216 -0
- package/dist/server/repositories/notes.js +57 -15
- package/dist/server/repositories/preferences.js +124 -0
- package/dist/server/repositories/questionnaires.js +1338 -0
- package/dist/server/repositories/settings.js +108 -12
- package/dist/server/repositories/surface-layouts.js +76 -0
- package/dist/server/repositories/wiki-memory.js +5 -1
- package/dist/server/services/entity-crud.js +81 -2
- package/dist/server/services/openai-codex-oauth.js +153 -0
- package/dist/server/services/psyche-observation-calendar.js +46 -0
- package/dist/server/types.js +492 -3
- package/dist/server/watch-mobile.js +562 -0
- package/dist/server/web.js +9 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +6 -1
- package/server/migrations/024_questionnaires.sql +96 -0
- package/server/migrations/025_ai_model_connections.sql +26 -0
- package/server/migrations/026_custom_theme_settings.sql +2 -0
- package/server/migrations/027_ai_processors.sql +31 -0
- package/server/migrations/028_movement_domain.sql +136 -0
- package/server/migrations/029_watch_micro_capture.sql +23 -0
- package/server/migrations/030_surface_layouts.sql +5 -0
- package/server/migrations/031_ai_processor_runtime_upgrades.sql +10 -0
- package/server/migrations/032_ai_connectors.sql +44 -0
- package/server/migrations/033_movement_trip_point_sync.sql +36 -0
- package/server/migrations/034_movement_segment_sync.sql +49 -0
- package/skills/forge-openclaw/SKILL.md +12 -1
- package/skills/forge-openclaw/entity_conversation_playbooks.md +331 -84
- package/skills/forge-openclaw/psyche_entity_playbooks.md +252 -221
- package/dist/assets/index-DTCwBWAs.js +0 -65
- package/dist/assets/index-DTCwBWAs.js.map +0 -1
- package/dist/assets/index-DttXlAgi.css +0 -1
- package/dist/assets/motion-D4sZgCHd.js.map +0 -1
- package/dist/assets/vendor-De38P6YR.js +0 -729
- package/dist/assets/vendor-De38P6YR.js.map +0 -1
- package/dist/assets/viz-C6hfyqzu.js +0 -34
- package/dist/assets/viz-C6hfyqzu.js.map +0 -1
- package/skills/forge-openclaw/cron_jobs.md +0 -395
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { getDatabase } from "../db.js";
|
|
3
|
+
import { aiModelConnectionSchema, upsertAiModelConnectionSchema } from "../types.js";
|
|
4
|
+
import { deleteEncryptedSecret, readEncryptedSecret, storeEncryptedSecret } from "./calendar.js";
|
|
5
|
+
import { upsertWikiLlmProfile } from "./wiki-memory.js";
|
|
6
|
+
export const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
7
|
+
export const DEFAULT_OPENAI_CODEX_BASE_URL = "https://chatgpt.com/backend-api";
|
|
8
|
+
export const FORGE_MANAGED_WIKI_PROFILE_ID = "wiki_llm_forge_managed";
|
|
9
|
+
export const FORGE_DEFAULT_AGENT_ID = "agt_forge_default";
|
|
10
|
+
function parseMetadata(value) {
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(value);
|
|
13
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function defaultAuthMode(provider) {
|
|
20
|
+
return provider === "openai-codex" ? "oauth" : "api_key";
|
|
21
|
+
}
|
|
22
|
+
export function defaultBaseUrlForProvider(provider) {
|
|
23
|
+
if (provider === "openai-codex") {
|
|
24
|
+
return DEFAULT_OPENAI_CODEX_BASE_URL;
|
|
25
|
+
}
|
|
26
|
+
if (provider === "openai-compatible") {
|
|
27
|
+
return "http://127.0.0.1:11434/v1";
|
|
28
|
+
}
|
|
29
|
+
return DEFAULT_OPENAI_BASE_URL;
|
|
30
|
+
}
|
|
31
|
+
function buildConnectionAgentId(connectionId) {
|
|
32
|
+
return `agt_model_${connectionId.replace(/[^a-zA-Z0-9]+/g, "_")}`;
|
|
33
|
+
}
|
|
34
|
+
export function buildConnectionAgentIdentity(connection) {
|
|
35
|
+
const detail = connection.provider === "openai-codex"
|
|
36
|
+
? "Chat agent backed by OpenAI Codex OAuth."
|
|
37
|
+
: connection.provider === "openai-compatible"
|
|
38
|
+
? "Chat agent backed by a local or OpenAI-compatible endpoint."
|
|
39
|
+
: "Chat agent backed by the OpenAI API.";
|
|
40
|
+
return {
|
|
41
|
+
id: connection.agentId,
|
|
42
|
+
label: connection.agentLabel,
|
|
43
|
+
agentType: connection.provider,
|
|
44
|
+
trustLevel: "trusted",
|
|
45
|
+
autonomyMode: "approval_required",
|
|
46
|
+
approvalMode: "approval_by_default",
|
|
47
|
+
description: detail,
|
|
48
|
+
tokenCount: 0,
|
|
49
|
+
activeTokenCount: 0,
|
|
50
|
+
createdAt: connection.createdAt,
|
|
51
|
+
updatedAt: connection.updatedAt
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function mapConnection(row) {
|
|
55
|
+
const hasStoredCredential = Boolean(row.secret_id) && Boolean(readEncryptedSecret(row.secret_id));
|
|
56
|
+
return aiModelConnectionSchema.parse({
|
|
57
|
+
id: row.id,
|
|
58
|
+
label: row.label,
|
|
59
|
+
provider: row.provider,
|
|
60
|
+
authMode: row.auth_mode,
|
|
61
|
+
baseUrl: row.base_url,
|
|
62
|
+
model: row.model,
|
|
63
|
+
accountLabel: row.account_label,
|
|
64
|
+
enabled: row.enabled === 1,
|
|
65
|
+
status: hasStoredCredential ? "connected" : "needs_attention",
|
|
66
|
+
hasStoredCredential,
|
|
67
|
+
usesOAuth: row.auth_mode === "oauth",
|
|
68
|
+
supportsCustomBaseUrl: row.provider !== "openai-codex",
|
|
69
|
+
agentId: buildConnectionAgentId(row.id),
|
|
70
|
+
agentLabel: `${row.label} agent`,
|
|
71
|
+
createdAt: row.created_at,
|
|
72
|
+
updatedAt: row.updated_at
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
export function listAiModelConnections() {
|
|
76
|
+
const rows = getDatabase()
|
|
77
|
+
.prepare(`SELECT id, label, provider, auth_mode, base_url, model, account_label, secret_id, enabled, metadata_json, created_at, updated_at
|
|
78
|
+
FROM ai_model_connections
|
|
79
|
+
ORDER BY created_at DESC`)
|
|
80
|
+
.all();
|
|
81
|
+
return rows.map(mapConnection);
|
|
82
|
+
}
|
|
83
|
+
export function getAiModelConnectionById(connectionId) {
|
|
84
|
+
const row = getDatabase()
|
|
85
|
+
.prepare(`SELECT id, label, provider, auth_mode, base_url, model, account_label, secret_id, enabled, metadata_json, created_at, updated_at
|
|
86
|
+
FROM ai_model_connections
|
|
87
|
+
WHERE id = ?`)
|
|
88
|
+
.get(connectionId);
|
|
89
|
+
return row ? mapConnection(row) : null;
|
|
90
|
+
}
|
|
91
|
+
export function readModelConnectionCredential(connectionId, secrets) {
|
|
92
|
+
const row = getDatabase()
|
|
93
|
+
.prepare(`SELECT secret_id
|
|
94
|
+
FROM ai_model_connections
|
|
95
|
+
WHERE id = ?`)
|
|
96
|
+
.get(connectionId);
|
|
97
|
+
if (!row?.secret_id) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const cipherText = readEncryptedSecret(row.secret_id);
|
|
101
|
+
if (!cipherText) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return secrets.openJson(cipherText);
|
|
105
|
+
}
|
|
106
|
+
export function upsertAiModelConnection(input, secrets, options = {}) {
|
|
107
|
+
const parsed = upsertAiModelConnectionSchema.parse(input);
|
|
108
|
+
const existing = parsed.id?.trim()
|
|
109
|
+
? getDatabase()
|
|
110
|
+
.prepare(`SELECT id, label, provider, auth_mode, base_url, model, account_label, secret_id, enabled, metadata_json, created_at, updated_at
|
|
111
|
+
FROM ai_model_connections
|
|
112
|
+
WHERE id = ?`)
|
|
113
|
+
.get(parsed.id.trim())
|
|
114
|
+
: undefined;
|
|
115
|
+
const id = existing?.id ??
|
|
116
|
+
`mdl_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
|
|
117
|
+
const now = new Date().toISOString();
|
|
118
|
+
const provider = parsed.provider;
|
|
119
|
+
const authMode = parsed.authMode ?? defaultAuthMode(provider);
|
|
120
|
+
const baseUrl = parsed.baseUrl?.trim() ||
|
|
121
|
+
existing?.base_url ||
|
|
122
|
+
defaultBaseUrlForProvider(provider);
|
|
123
|
+
let secretId = existing?.secret_id ?? null;
|
|
124
|
+
let accountLabel = existing?.account_label ?? null;
|
|
125
|
+
if (parsed.apiKey?.trim()) {
|
|
126
|
+
secretId =
|
|
127
|
+
secretId ?? `mdl_secret_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
|
|
128
|
+
storeEncryptedSecret(secretId, secrets.sealJson({
|
|
129
|
+
kind: "api_key",
|
|
130
|
+
provider,
|
|
131
|
+
apiKey: parsed.apiKey.trim()
|
|
132
|
+
}), `${parsed.label} AI connection`);
|
|
133
|
+
}
|
|
134
|
+
else if (options.oauthCredential) {
|
|
135
|
+
secretId =
|
|
136
|
+
secretId ?? `mdl_secret_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
|
|
137
|
+
accountLabel = options.oauthCredential.accountId;
|
|
138
|
+
storeEncryptedSecret(secretId, secrets.sealJson(options.oauthCredential), `${parsed.label} AI OAuth connection`);
|
|
139
|
+
}
|
|
140
|
+
getDatabase()
|
|
141
|
+
.prepare(`INSERT INTO ai_model_connections (
|
|
142
|
+
id, label, provider, auth_mode, base_url, model, account_label, secret_id, enabled, metadata_json, created_at, updated_at
|
|
143
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
144
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
145
|
+
label = excluded.label,
|
|
146
|
+
provider = excluded.provider,
|
|
147
|
+
auth_mode = excluded.auth_mode,
|
|
148
|
+
base_url = excluded.base_url,
|
|
149
|
+
model = excluded.model,
|
|
150
|
+
account_label = excluded.account_label,
|
|
151
|
+
secret_id = excluded.secret_id,
|
|
152
|
+
enabled = excluded.enabled,
|
|
153
|
+
metadata_json = excluded.metadata_json,
|
|
154
|
+
updated_at = excluded.updated_at`)
|
|
155
|
+
.run(id, parsed.label, provider, authMode, baseUrl, parsed.model.trim(), accountLabel, secretId, parsed.enabled ? 1 : 0, JSON.stringify(parseMetadata(existing?.metadata_json ?? "{}")), existing?.created_at ?? now, now);
|
|
156
|
+
return getAiModelConnectionById(id);
|
|
157
|
+
}
|
|
158
|
+
export function deleteAiModelConnection(connectionId, secrets) {
|
|
159
|
+
const row = getDatabase()
|
|
160
|
+
.prepare(`SELECT id, secret_id
|
|
161
|
+
FROM ai_model_connections
|
|
162
|
+
WHERE id = ?`)
|
|
163
|
+
.get(connectionId);
|
|
164
|
+
if (!row) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
getDatabase()
|
|
168
|
+
.prepare(`DELETE FROM ai_model_connections WHERE id = ?`)
|
|
169
|
+
.run(connectionId);
|
|
170
|
+
if (row.secret_id) {
|
|
171
|
+
deleteEncryptedSecret(row.secret_id);
|
|
172
|
+
}
|
|
173
|
+
getDatabase()
|
|
174
|
+
.prepare(`UPDATE app_settings
|
|
175
|
+
SET forge_basic_chat_connection_id = CASE WHEN forge_basic_chat_connection_id = ? THEN '' ELSE forge_basic_chat_connection_id END,
|
|
176
|
+
forge_wiki_connection_id = CASE WHEN forge_wiki_connection_id = ? THEN '' ELSE forge_wiki_connection_id END,
|
|
177
|
+
updated_at = ?
|
|
178
|
+
WHERE id = 1`)
|
|
179
|
+
.run(connectionId, connectionId, new Date().toISOString());
|
|
180
|
+
syncForgeManagedWikiProfile(secrets);
|
|
181
|
+
return row.id;
|
|
182
|
+
}
|
|
183
|
+
export function syncForgeManagedWikiProfile(secrets) {
|
|
184
|
+
const settings = getDatabase()
|
|
185
|
+
.prepare(`SELECT forge_wiki_connection_id, forge_wiki_model
|
|
186
|
+
FROM app_settings
|
|
187
|
+
WHERE id = 1`)
|
|
188
|
+
.get();
|
|
189
|
+
const connectionId = settings?.forge_wiki_connection_id?.trim() ?? "";
|
|
190
|
+
const fallbackModel = settings?.forge_wiki_model?.trim() || "gpt-5.4-mini";
|
|
191
|
+
const connection = connectionId ? getAiModelConnectionById(connectionId) : null;
|
|
192
|
+
const row = connectionId
|
|
193
|
+
? getDatabase()
|
|
194
|
+
.prepare(`SELECT secret_id
|
|
195
|
+
FROM ai_model_connections
|
|
196
|
+
WHERE id = ?`)
|
|
197
|
+
.get(connectionId)
|
|
198
|
+
: undefined;
|
|
199
|
+
upsertWikiLlmProfile({
|
|
200
|
+
id: FORGE_MANAGED_WIKI_PROFILE_ID,
|
|
201
|
+
label: "Forge wiki ingest",
|
|
202
|
+
provider: connection?.provider === "openai-compatible"
|
|
203
|
+
? "openai-compatible"
|
|
204
|
+
: connection?.provider === "openai-codex"
|
|
205
|
+
? "openai-codex"
|
|
206
|
+
: "openai-responses",
|
|
207
|
+
baseUrl: connection?.baseUrl ?? DEFAULT_OPENAI_BASE_URL,
|
|
208
|
+
model: connection?.model ?? fallbackModel,
|
|
209
|
+
secretId: row?.secret_id ?? null,
|
|
210
|
+
enabled: true,
|
|
211
|
+
metadata: {
|
|
212
|
+
managedBySettings: true,
|
|
213
|
+
connectionId: connection?.id ?? null
|
|
214
|
+
}
|
|
215
|
+
}, secrets);
|
|
216
|
+
}
|
|
@@ -80,6 +80,22 @@ function noteMatchesTextTerm(note, term) {
|
|
|
80
80
|
}
|
|
81
81
|
return note.tags.some((tag) => tag.toLowerCase().includes(normalized));
|
|
82
82
|
}
|
|
83
|
+
function filterNotesByOwnerIds(notes, userIds) {
|
|
84
|
+
if (!userIds || userIds.length === 0) {
|
|
85
|
+
return notes;
|
|
86
|
+
}
|
|
87
|
+
const allowed = new Set(userIds);
|
|
88
|
+
return notes.filter((note) => note.userId !== null && allowed.has(note.userId));
|
|
89
|
+
}
|
|
90
|
+
export function resolveNoteObservedAt(note) {
|
|
91
|
+
const observedAt = typeof note.frontmatter.observedAt === "string"
|
|
92
|
+
? note.frontmatter.observedAt.trim()
|
|
93
|
+
: "";
|
|
94
|
+
if (observedAt.length > 0 && !Number.isNaN(Date.parse(observedAt))) {
|
|
95
|
+
return new Date(observedAt).toISOString();
|
|
96
|
+
}
|
|
97
|
+
return note.createdAt;
|
|
98
|
+
}
|
|
83
99
|
function cleanupExpiredNotes() {
|
|
84
100
|
const expiredRows = getDatabase()
|
|
85
101
|
.prepare(`SELECT id
|
|
@@ -198,6 +214,16 @@ function listAllNoteRows() {
|
|
|
198
214
|
ORDER BY created_at DESC`)
|
|
199
215
|
.all();
|
|
200
216
|
}
|
|
217
|
+
function listActiveNotes() {
|
|
218
|
+
const rows = listAllNoteRows();
|
|
219
|
+
const linksByNoteId = new Map();
|
|
220
|
+
for (const link of listLinkRowsForNotes(rows.map((row) => row.id))) {
|
|
221
|
+
const current = linksByNoteId.get(link.note_id) ?? [];
|
|
222
|
+
current.push(link);
|
|
223
|
+
linksByNoteId.set(link.note_id, current);
|
|
224
|
+
}
|
|
225
|
+
return filterDeletedEntities("note", rows.map((row) => mapNote(row, linksByNoteId.get(row.id) ?? [])));
|
|
226
|
+
}
|
|
201
227
|
function findMatchingNoteIds(query) {
|
|
202
228
|
const ftsQuery = buildFtsQuery(query);
|
|
203
229
|
if (!ftsQuery) {
|
|
@@ -313,23 +339,17 @@ export function listNotes(query = {}) {
|
|
|
313
339
|
...(parsed.query ? [parsed.query] : []),
|
|
314
340
|
...parsed.textTerms
|
|
315
341
|
]);
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
.filter((row) => parsed.kind ? row.kind === parsed.kind : true)
|
|
325
|
-
.filter((row) => parsed.spaceId ? row.space_id === parsed.spaceId : true)
|
|
326
|
-
.filter((row) => parsed.slug ? row.slug.toLowerCase() === parsed.slug.toLowerCase() : true)
|
|
327
|
-
.filter((row) => parsed.author
|
|
328
|
-
? (row.author ?? "")
|
|
342
|
+
return filterNotesByOwnerIds(listActiveNotes()
|
|
343
|
+
.filter((note) => parsed.kind ? note.kind === parsed.kind : true)
|
|
344
|
+
.filter((note) => parsed.spaceId ? note.spaceId === parsed.spaceId : true)
|
|
345
|
+
.filter((note) => parsed.slug
|
|
346
|
+
? note.slug.toLowerCase() === parsed.slug.toLowerCase()
|
|
347
|
+
: true)
|
|
348
|
+
.filter((note) => parsed.author
|
|
349
|
+
? (note.author ?? "")
|
|
329
350
|
.toLowerCase()
|
|
330
351
|
.includes(parsed.author.toLowerCase())
|
|
331
352
|
: true)
|
|
332
|
-
.map((row) => mapNote(row, linksByNoteId.get(row.id) ?? []))
|
|
333
353
|
.filter((note) => {
|
|
334
354
|
if (!matchingIds) {
|
|
335
355
|
return true;
|
|
@@ -361,7 +381,29 @@ export function listNotes(query = {}) {
|
|
|
361
381
|
}
|
|
362
382
|
return true;
|
|
363
383
|
})
|
|
364
|
-
.slice(0, parsed.limit ?? 100));
|
|
384
|
+
.slice(0, parsed.limit ?? 100), parsed.userIds);
|
|
385
|
+
}
|
|
386
|
+
export function listNotesByObservedAtRange({ from, to, userIds, limit = 400 }) {
|
|
387
|
+
cleanupExpiredNotes();
|
|
388
|
+
const fromMs = Date.parse(from);
|
|
389
|
+
const toMs = Date.parse(to);
|
|
390
|
+
if (Number.isNaN(fromMs) || Number.isNaN(toMs)) {
|
|
391
|
+
return [];
|
|
392
|
+
}
|
|
393
|
+
return filterNotesByOwnerIds(listActiveNotes(), userIds)
|
|
394
|
+
.map((note) => ({
|
|
395
|
+
note,
|
|
396
|
+
observedAt: resolveNoteObservedAt(note)
|
|
397
|
+
}))
|
|
398
|
+
.filter(({ observedAt }) => {
|
|
399
|
+
const observedAtMs = Date.parse(observedAt);
|
|
400
|
+
return (!Number.isNaN(observedAtMs) &&
|
|
401
|
+
observedAtMs >= fromMs &&
|
|
402
|
+
observedAtMs < toMs);
|
|
403
|
+
})
|
|
404
|
+
.sort((left, right) => left.observedAt.localeCompare(right.observedAt))
|
|
405
|
+
.slice(0, limit)
|
|
406
|
+
.map(({ note }) => note);
|
|
365
407
|
}
|
|
366
408
|
export function createNote(input, context) {
|
|
367
409
|
cleanupExpiredNotes();
|
|
@@ -444,6 +444,53 @@ function listStoredScores(contextId) {
|
|
|
444
444
|
WHERE context_id = ?`)
|
|
445
445
|
.all(contextId);
|
|
446
446
|
}
|
|
447
|
+
export function listPreferenceContexts() {
|
|
448
|
+
return getDatabase()
|
|
449
|
+
.prepare(`SELECT id, profile_id, name, description, share_mode, active, is_default, decay_days, created_at, updated_at
|
|
450
|
+
FROM preference_contexts
|
|
451
|
+
ORDER BY created_at ASC`)
|
|
452
|
+
.all().map(mapContext);
|
|
453
|
+
}
|
|
454
|
+
export function getPreferenceContextById(contextId) {
|
|
455
|
+
return readContext(contextId) ?? undefined;
|
|
456
|
+
}
|
|
457
|
+
export function listPreferenceItems() {
|
|
458
|
+
return getDatabase()
|
|
459
|
+
.prepare(`SELECT id, profile_id, label, description, tags_json, feature_weights_json, source_entity_type, source_entity_id, metadata_json, created_at, updated_at
|
|
460
|
+
FROM preference_items
|
|
461
|
+
ORDER BY created_at ASC`)
|
|
462
|
+
.all().map(mapItem);
|
|
463
|
+
}
|
|
464
|
+
export function getPreferenceItemById(itemId) {
|
|
465
|
+
return getItemById(itemId) ?? undefined;
|
|
466
|
+
}
|
|
467
|
+
export function listPreferenceCatalogs() {
|
|
468
|
+
return getDatabase()
|
|
469
|
+
.prepare(`SELECT id, profile_id, domain, slug, title, description, source, archived, created_at, updated_at
|
|
470
|
+
FROM preference_catalogs
|
|
471
|
+
WHERE archived = 0
|
|
472
|
+
ORDER BY created_at ASC`)
|
|
473
|
+
.all()
|
|
474
|
+
.filter((row) => row.archived === 0)
|
|
475
|
+
.map((row) => readCatalog(row.id))
|
|
476
|
+
.filter((catalog) => catalog !== null);
|
|
477
|
+
}
|
|
478
|
+
export function getPreferenceCatalogById(catalogId) {
|
|
479
|
+
return readCatalog(catalogId) ?? undefined;
|
|
480
|
+
}
|
|
481
|
+
export function listPreferenceCatalogItems() {
|
|
482
|
+
return getDatabase()
|
|
483
|
+
.prepare(`SELECT id, catalog_id, label, description, tags_json, feature_weights_json, position, archived, created_at, updated_at
|
|
484
|
+
FROM preference_catalog_items
|
|
485
|
+
WHERE archived = 0
|
|
486
|
+
ORDER BY catalog_id ASC, position ASC, created_at ASC`)
|
|
487
|
+
.all()
|
|
488
|
+
.filter((row) => row.archived === 0)
|
|
489
|
+
.map(mapCatalogItem);
|
|
490
|
+
}
|
|
491
|
+
export function getPreferenceCatalogItemById(catalogItemId) {
|
|
492
|
+
return readCatalogItem(catalogItemId) ?? undefined;
|
|
493
|
+
}
|
|
447
494
|
function listStoredDimensions(contextId) {
|
|
448
495
|
return getDatabase()
|
|
449
496
|
.prepare(`SELECT id, profile_id, context_id, dimension_id, leaning, confidence, movement, context_sensitivity, evidence_count, updated_at
|
|
@@ -1535,6 +1582,55 @@ export function updatePreferenceContext(contextId, patch) {
|
|
|
1535
1582
|
}
|
|
1536
1583
|
return updated;
|
|
1537
1584
|
}
|
|
1585
|
+
export function deletePreferenceContext(contextId) {
|
|
1586
|
+
const current = readContext(contextId);
|
|
1587
|
+
if (!current) {
|
|
1588
|
+
throw new HttpError(404, "preferences_context_not_found", `Preference context ${contextId} was not found.`);
|
|
1589
|
+
}
|
|
1590
|
+
const remainingContexts = listContexts(current.profileId).filter((entry) => entry.id !== contextId);
|
|
1591
|
+
if (remainingContexts.length === 0) {
|
|
1592
|
+
throw new HttpError(400, "preferences_context_last_remaining", "A preference profile must keep at least one context.");
|
|
1593
|
+
}
|
|
1594
|
+
const replacementDefault = remainingContexts.find((entry) => entry.isDefault) ?? remainingContexts[0];
|
|
1595
|
+
const timestamp = nowIso();
|
|
1596
|
+
runInTransaction(() => {
|
|
1597
|
+
getDatabase()
|
|
1598
|
+
.prepare(`DELETE FROM pairwise_judgments WHERE context_id = ?`)
|
|
1599
|
+
.run(contextId);
|
|
1600
|
+
getDatabase()
|
|
1601
|
+
.prepare(`DELETE FROM absolute_signals WHERE context_id = ?`)
|
|
1602
|
+
.run(contextId);
|
|
1603
|
+
getDatabase()
|
|
1604
|
+
.prepare(`DELETE FROM preference_item_scores WHERE context_id = ?`)
|
|
1605
|
+
.run(contextId);
|
|
1606
|
+
getDatabase()
|
|
1607
|
+
.prepare(`DELETE FROM preference_dimension_summaries WHERE context_id = ?`)
|
|
1608
|
+
.run(contextId);
|
|
1609
|
+
getDatabase()
|
|
1610
|
+
.prepare(`DELETE FROM preference_snapshots WHERE context_id = ?`)
|
|
1611
|
+
.run(contextId);
|
|
1612
|
+
getDatabase()
|
|
1613
|
+
.prepare(`DELETE FROM preference_contexts WHERE id = ?`)
|
|
1614
|
+
.run(contextId);
|
|
1615
|
+
if (current.isDefault) {
|
|
1616
|
+
getDatabase()
|
|
1617
|
+
.prepare(`UPDATE preference_contexts
|
|
1618
|
+
SET is_default = CASE WHEN id = ? THEN 1 ELSE 0 END, updated_at = ?
|
|
1619
|
+
WHERE profile_id = ?`)
|
|
1620
|
+
.run(replacementDefault.id, timestamp, current.profileId);
|
|
1621
|
+
getDatabase()
|
|
1622
|
+
.prepare(`UPDATE preference_profiles
|
|
1623
|
+
SET default_context_id = ?, updated_at = ?
|
|
1624
|
+
WHERE id = ?`)
|
|
1625
|
+
.run(replacementDefault.id, timestamp, current.profileId);
|
|
1626
|
+
}
|
|
1627
|
+
});
|
|
1628
|
+
const profile = readProfileById(current.profileId);
|
|
1629
|
+
if (profile) {
|
|
1630
|
+
recomputeContext(profile, replacementDefault);
|
|
1631
|
+
}
|
|
1632
|
+
return current;
|
|
1633
|
+
}
|
|
1538
1634
|
export function mergePreferenceContexts(input) {
|
|
1539
1635
|
const parsed = mergePreferenceContextsSchema.parse(input);
|
|
1540
1636
|
const source = readContext(parsed.sourceContextId);
|
|
@@ -1661,6 +1757,34 @@ export function updatePreferenceItem(itemId, patch) {
|
|
|
1661
1757
|
}
|
|
1662
1758
|
return updated;
|
|
1663
1759
|
}
|
|
1760
|
+
export function deletePreferenceItem(itemId) {
|
|
1761
|
+
const current = getItemById(itemId);
|
|
1762
|
+
if (!current) {
|
|
1763
|
+
throw new HttpError(404, "preferences_item_not_found", `Preference item ${itemId} was not found.`);
|
|
1764
|
+
}
|
|
1765
|
+
runInTransaction(() => {
|
|
1766
|
+
getDatabase()
|
|
1767
|
+
.prepare(`DELETE FROM pairwise_judgments
|
|
1768
|
+
WHERE left_item_id = ? OR right_item_id = ?`)
|
|
1769
|
+
.run(itemId, itemId);
|
|
1770
|
+
getDatabase()
|
|
1771
|
+
.prepare(`DELETE FROM absolute_signals WHERE item_id = ?`)
|
|
1772
|
+
.run(itemId);
|
|
1773
|
+
getDatabase()
|
|
1774
|
+
.prepare(`DELETE FROM preference_item_scores WHERE item_id = ?`)
|
|
1775
|
+
.run(itemId);
|
|
1776
|
+
getDatabase()
|
|
1777
|
+
.prepare(`DELETE FROM preference_items WHERE id = ?`)
|
|
1778
|
+
.run(itemId);
|
|
1779
|
+
});
|
|
1780
|
+
const profile = readProfileById(current.profileId);
|
|
1781
|
+
if (profile) {
|
|
1782
|
+
for (const context of listContexts(profile.id).filter((entry) => entry.active)) {
|
|
1783
|
+
recomputeContext(profile, context);
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
return current;
|
|
1787
|
+
}
|
|
1664
1788
|
export function createPreferenceItemFromEntity(input) {
|
|
1665
1789
|
const parsed = enqueueEntityPreferenceItemSchema.parse(input);
|
|
1666
1790
|
const profile = ensureProfile(parsed.userId, parsed.domain);
|