forge-openclaw-plugin 0.2.20 → 0.2.21
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/dist/assets/{board-DGbXWEuu.js → board-_C6oMy5w.js} +2 -2
- package/dist/assets/{board-DGbXWEuu.js.map → board-_C6oMy5w.js.map} +1 -1
- package/dist/assets/index-B4A6TooJ.js +63 -0
- package/dist/assets/index-B4A6TooJ.js.map +1 -0
- package/dist/assets/index-D6Xs_2mo.css +1 -0
- package/dist/assets/{motion-B5Qoz2Ci.js → motion-D4sZgCHd.js} +2 -2
- package/dist/assets/{motion-B5Qoz2Ci.js.map → motion-D4sZgCHd.js.map} +1 -1
- package/dist/assets/{table-D_iurDQu.js → table-BWzTaky1.js} +2 -2
- package/dist/assets/{table-D_iurDQu.js.map → table-BWzTaky1.js.map} +1 -1
- package/dist/assets/{ui-D5QUYUq4.js → ui-BzK4azQb.js} +2 -2
- package/dist/assets/{ui-D5QUYUq4.js.map → ui-BzK4azQb.js.map} +1 -1
- package/dist/assets/vendor-De38P6YR.js +729 -0
- package/dist/assets/vendor-De38P6YR.js.map +1 -0
- package/dist/assets/{viz-BD9WSxHz.js → viz-C6hfyqzu.js} +2 -2
- package/dist/assets/{viz-BD9WSxHz.js.map → viz-C6hfyqzu.js.map} +1 -1
- package/dist/index.html +8 -8
- package/dist/server/app.js +301 -19
- package/dist/server/health.js +82 -21
- package/dist/server/managers/platform/background-job-manager.js +103 -8
- package/dist/server/managers/platform/llm-manager.js +91 -5
- package/dist/server/managers/platform/openai-responses-provider.js +683 -70
- package/dist/server/repositories/diagnostic-logs.js +243 -0
- package/dist/server/repositories/wiki-memory.js +595 -62
- package/dist/server/types.js +56 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server/migrations/023_diagnostic_logs.sql +28 -0
- package/skills/forge-openclaw/SKILL.md +14 -0
- package/dist/assets/index-4-1WI9i7.css +0 -1
- package/dist/assets/index-BZbHajNK.js +0 -63
- package/dist/assets/index-BZbHajNK.js.map +0 -1
- package/dist/assets/vendor-KARp8LAR.js +0 -706
- package/dist/assets/vendor-KARp8LAR.js.map +0 -1
package/dist/index.html
CHANGED
|
@@ -13,15 +13,15 @@
|
|
|
13
13
|
/>
|
|
14
14
|
<link rel="icon" type="image/png" href="/forge/assets/favicon-BCHm9dUV.ico" />
|
|
15
15
|
<link rel="alternate icon" href="/forge/assets/favicon-BCHm9dUV.ico" />
|
|
16
|
-
<script type="module" crossorigin src="/forge/assets/index-
|
|
17
|
-
<link rel="modulepreload" crossorigin href="/forge/assets/viz-
|
|
18
|
-
<link rel="modulepreload" crossorigin href="/forge/assets/vendor-
|
|
19
|
-
<link rel="modulepreload" crossorigin href="/forge/assets/ui-
|
|
20
|
-
<link rel="modulepreload" crossorigin href="/forge/assets/motion-
|
|
21
|
-
<link rel="modulepreload" crossorigin href="/forge/assets/table-
|
|
22
|
-
<link rel="modulepreload" crossorigin href="/forge/assets/board-
|
|
16
|
+
<script type="module" crossorigin src="/forge/assets/index-B4A6TooJ.js"></script>
|
|
17
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/viz-C6hfyqzu.js">
|
|
18
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/vendor-De38P6YR.js">
|
|
19
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/ui-BzK4azQb.js">
|
|
20
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/motion-D4sZgCHd.js">
|
|
21
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/table-BWzTaky1.js">
|
|
22
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/board-_C6oMy5w.js">
|
|
23
23
|
<link rel="stylesheet" crossorigin href="/forge/assets/vendor-DT3pnAKJ.css">
|
|
24
|
-
<link rel="stylesheet" crossorigin href="/forge/assets/index-
|
|
24
|
+
<link rel="stylesheet" crossorigin href="/forge/assets/index-D6Xs_2mo.css">
|
|
25
25
|
</head>
|
|
26
26
|
<body class="bg-canvas text-ink antialiased">
|
|
27
27
|
<div id="root"></div>
|
package/dist/server/app.js
CHANGED
|
@@ -7,11 +7,12 @@ import { HttpError, isHttpError } from "./errors.js";
|
|
|
7
7
|
import { listActivityEvents, listActivityEventsForTask, recordActivityEvent, removeActivityEvent } from "./repositories/activity-events.js";
|
|
8
8
|
import { approveApprovalRequest, createAgentAction, createInsight, createInsightFeedback, deleteInsight, getInsightById, listAgentActions, listApprovalRequests, listInsights, rejectApprovalRequest, updateInsight } from "./repositories/collaboration.js";
|
|
9
9
|
import { listEventLog } from "./repositories/event-log.js";
|
|
10
|
+
import { createDiagnosticMessage, DIAGNOSTIC_LOG_RETENTION_SWEEP_INTERVAL_MS, enforceDiagnosticLogRetention, listDiagnosticLogs, normalizeDiagnosticSource, recordDiagnosticLog, serializeDiagnosticError } from "./repositories/diagnostic-logs.js";
|
|
10
11
|
import { createGoal, getGoalById, listGoals, updateGoal } from "./repositories/goals.js";
|
|
11
12
|
import { createHabit, createHabitCheckIn, deleteHabitCheckIn, getHabitById, listHabits, updateHabit } from "./repositories/habits.js";
|
|
12
13
|
import { listDomains } from "./repositories/domains.js";
|
|
13
14
|
import { buildNotesSummaryByEntity, createNote, getNoteById, listNotes, updateNote } from "./repositories/notes.js";
|
|
14
|
-
import { createWikiIngestJobSchema, createUploadedWikiIngestJob, createWikiSpace, createWikiSpaceSchema, deleteWikiProfile, getWikiHealth, getWikiIngestJob, getWikiHomePageDetail, getWikiPageDetail, getWikiPageDetailBySlug, getWikiSettingsPayload, ingestWikiSource, listWikiIngestJobs, listWikiPageTree, listWikiPages, listWikiSpaces, processWikiIngestJob, reindexWikiEmbeddings, reindexWikiEmbeddingsSchema, reviewWikiIngestJob, reviewWikiIngestJobSchema, searchWikiPages, syncWikiVaultFromDisk, syncWikiVaultSchema, upsertWikiEmbeddingProfile, upsertWikiEmbeddingProfileSchema, upsertWikiLlmProfile, upsertWikiLlmProfileSchema, wikiSearchQuerySchema } from "./repositories/wiki-memory.js";
|
|
15
|
+
import { createWikiIngestJobSchema, createUploadedWikiIngestJob, createWikiSpace, createWikiSpaceSchema, deleteWikiIngestJob, deleteWikiProfile, getWikiHealth, getWikiIngestJob, getWikiHomePageDetail, getWikiPageDetail, getWikiPageDetailBySlug, getWikiSettingsPayload, ingestWikiSource, listWikiIngestJobs, listWikiLlmProfiles, listWikiPageTree, listWikiPages, listWikiSpaces, processWikiIngestJob, reindexWikiEmbeddings, reindexWikiEmbeddingsSchema, rerunWikiIngestJob, reviewWikiIngestJob, reviewWikiIngestJobSchema, searchWikiPages, syncWikiVaultFromDisk, syncWikiVaultSchema, testWikiLlmProfileSchema, upsertWikiEmbeddingProfile, upsertWikiEmbeddingProfileSchema, upsertWikiLlmProfile, upsertWikiLlmProfileSchema, wikiSearchQuerySchema } from "./repositories/wiki-memory.js";
|
|
15
16
|
import { filterOwnedEntities, setEntityOwner } from "./repositories/entity-ownership.js";
|
|
16
17
|
import { createBehavior, createBehaviorPattern, createBeliefEntry, createEmotionDefinition, createEventType, createModeGuideSession, createModeProfile, createPsycheValue, createTriggerReport, getBehaviorById, getBehaviorPatternById, getBeliefEntryById, getEmotionDefinitionById, getEventTypeById, getModeGuideSessionById, getModeProfileById, getPsycheValueById, getTriggerReportById, listBehaviors, listBehaviorPatterns, listBeliefEntries, listEmotionDefinitions, listEventTypes, listModeGuideSessions, listModeProfiles, listPsycheValues, listSchemaCatalog, listTriggerReports, updateBehavior, updateBehaviorPattern, updateBeliefEntry, updateEmotionDefinition, updateEventType, updateModeGuideSession, updateModeProfile, updatePsycheValue, updateTriggerReport } from "./repositories/psyche.js";
|
|
17
18
|
import { createProject, updateProject } from "./repositories/projects.js";
|
|
@@ -39,12 +40,12 @@ import { suggestTags } from "./services/tagging.js";
|
|
|
39
40
|
import { CalendarConnectionConflictError, completeMicrosoftCalendarOauth, createCalendarConnection, deleteCalendarEventProjection, discoverCalendarConnection, discoverExistingCalendarConnection, getMicrosoftCalendarOauthSession, listConnectedCalendarConnections, removeCalendarConnection, pushCalendarEventUpdate, readCalendarOverview, syncCalendarConnection, startMicrosoftCalendarOauth, testMicrosoftCalendarOauthConfiguration, listCalendarProviderMetadata, updateCalendarConnectionSelection } from "./services/calendar-runtime.js";
|
|
40
41
|
import { PSYCHE_ENTITY_TYPES, createBehaviorSchema, createBeliefEntrySchema, createBehaviorPatternSchema, createEmotionDefinitionSchema, createEventTypeSchema, createModeGuideSessionSchema, createModeProfileSchema, createPsycheValueSchema, createTriggerReportSchema, updateBehaviorSchema, updateBeliefEntrySchema, updateBehaviorPatternSchema, updateEmotionDefinitionSchema, updateEventTypeSchema, updateModeGuideSessionSchema, updateModeProfileSchema, updatePsycheValueSchema, updateTriggerReportSchema } from "./psyche-types.js";
|
|
41
42
|
import { createPreferenceCatalogItemSchema, createPreferenceCatalogSchema, createPreferenceContextSchema, createPreferenceItemSchema, enqueueEntityPreferenceItemSchema, mergePreferenceContextsSchema, preferenceWorkspaceQuerySchema, startPreferenceGameSchema, submitAbsoluteSignalSchema, submitPairwiseJudgmentSchema, updatePreferenceCatalogItemSchema, updatePreferenceCatalogSchema, updatePreferenceContextSchema, updatePreferenceItemSchema, updatePreferenceScoreSchema } from "./preferences-types.js";
|
|
42
|
-
import { activityListQuerySchema, activitySourceSchema, createAgentActionSchema, createAgentTokenSchema, batchCreateEntitiesSchema, batchDeleteEntitiesSchema, batchRestoreEntitiesSchema, batchSearchEntitiesSchema, batchUpdateEntitiesSchema, createGoalSchema, createInsightFeedbackSchema, createInsightSchema, createStrategySchema, createUserSchema, createNoteSchema, createProjectSchema, createManualRewardGrantSchema, createCalendarEventSchema, createHabitCheckInSchema, createCalendarConnectionSchema, discoverCalendarConnectionSchema, startMicrosoftCalendarOauthSchema, testMicrosoftCalendarOauthConfigurationSchema, createHabitSchema, createTaskTimeboxSchema, createWorkBlockTemplateSchema, createSessionEventSchema, createWorkAdjustmentSchema, createTagSchema, calendarOverviewQuerySchema, notesListQuerySchema, updateTagSchema, createTaskSchema, eventsListQuerySchema, operatorLogWorkSchema, projectBoardPayloadSchema, projectListQuerySchema, entityDeleteQuerySchema, removeActivityEventSchema, resolveApprovalRequestSchema, rewardsLedgerQuerySchema, habitListQuerySchema, taskContextPayloadSchema, taskRunClaimSchema, taskRunFocusSchema, taskRunFinishSchema, taskRunHeartbeatSchema, taskRunListQuerySchema, taskListQuerySchema, tagSuggestionRequestSchema, uncompleteTaskSchema, updateSettingsSchema, updateGoalSchema, updateHabitSchema, updateInsightSchema, updateStrategySchema, updateUserSchema, updateCalendarConnectionSchema, updateCalendarEventSchema, updateNoteSchema, updateProjectSchema, updateRewardRuleSchema, updateTaskTimeboxSchema, updateTaskSchema, updateUserAccessGrantSchema, updateWorkBlockTemplateSchema, workAdjustmentResultSchema, finalizeWeeklyReviewResultSchema, goalListQuerySchema, recommendTaskTimeboxesSchema, strategyListQuerySchema } from "./types.js";
|
|
43
|
+
import { activityListQuerySchema, activitySourceSchema, createAgentActionSchema, createAgentTokenSchema, batchCreateEntitiesSchema, batchDeleteEntitiesSchema, batchRestoreEntitiesSchema, batchSearchEntitiesSchema, batchUpdateEntitiesSchema, createGoalSchema, createInsightFeedbackSchema, createInsightSchema, createStrategySchema, createUserSchema, createNoteSchema, createProjectSchema, createManualRewardGrantSchema, createCalendarEventSchema, createHabitCheckInSchema, createCalendarConnectionSchema, createDiagnosticLogSchema, discoverCalendarConnectionSchema, startMicrosoftCalendarOauthSchema, testMicrosoftCalendarOauthConfigurationSchema, createHabitSchema, createTaskTimeboxSchema, createWorkBlockTemplateSchema, createSessionEventSchema, createWorkAdjustmentSchema, createTagSchema, calendarOverviewQuerySchema, notesListQuerySchema, updateTagSchema, createTaskSchema, diagnosticLogListQuerySchema, eventsListQuerySchema, operatorLogWorkSchema, projectBoardPayloadSchema, projectListQuerySchema, entityDeleteQuerySchema, removeActivityEventSchema, resolveApprovalRequestSchema, rewardsLedgerQuerySchema, habitListQuerySchema, taskContextPayloadSchema, taskRunClaimSchema, taskRunFocusSchema, taskRunFinishSchema, taskRunHeartbeatSchema, taskRunListQuerySchema, taskListQuerySchema, tagSuggestionRequestSchema, uncompleteTaskSchema, updateSettingsSchema, updateGoalSchema, updateHabitSchema, updateInsightSchema, updateStrategySchema, updateUserSchema, updateCalendarConnectionSchema, updateCalendarEventSchema, updateNoteSchema, updateProjectSchema, updateRewardRuleSchema, updateTaskTimeboxSchema, updateTaskSchema, updateUserAccessGrantSchema, updateWorkBlockTemplateSchema, workAdjustmentResultSchema, finalizeWeeklyReviewResultSchema, goalListQuerySchema, recommendTaskTimeboxesSchema, strategyListQuerySchema } from "./types.js";
|
|
43
44
|
import { buildOpenApiDocument } from "./openapi.js";
|
|
44
45
|
import { registerWebRoutes } from "./web.js";
|
|
45
46
|
import { createManagerRuntime } from "./managers/runtime.js";
|
|
46
47
|
import { isManagerError } from "./managers/type-guards.js";
|
|
47
|
-
import { createCompanionPairingSession, createCompanionPairingSessionSchema, getCompanionOverview, getFitnessViewData, getSleepViewData, ingestMobileHealthSync, mobileHealthSyncSchema, revokeCompanionPairingSession, verifyCompanionPairing, verifyCompanionPairingSchema, updateSleepMetadata, updateSleepMetadataSchema, updateWorkoutMetadata, updateWorkoutMetadataSchema } from "./health.js";
|
|
48
|
+
import { createCompanionPairingSession, createCompanionPairingSessionSchema, getCompanionOverview, getFitnessViewData, getSleepViewData, ingestMobileHealthSync, mobileHealthSyncSchema, revokeAllCompanionPairingSessions, revokeAllCompanionPairingSessionsSchema, revokeCompanionPairingSession, verifyCompanionPairing, verifyCompanionPairingSchema, updateSleepMetadata, updateSleepMetadataSchema, updateWorkoutMetadata, updateWorkoutMetadataSchema } from "./health.js";
|
|
48
49
|
const COMPATIBILITY_SUNSET = "transitional-node";
|
|
49
50
|
function markCompatibilityRoute(reply) {
|
|
50
51
|
reply.header("Deprecation", "true");
|
|
@@ -3517,11 +3518,93 @@ export async function buildServer(options = {}) {
|
|
|
3517
3518
|
credentials: true
|
|
3518
3519
|
});
|
|
3519
3520
|
await app.register(multipart);
|
|
3521
|
+
enforceDiagnosticLogRetention({ force: true });
|
|
3522
|
+
const diagnosticRetentionTimer = setInterval(() => {
|
|
3523
|
+
try {
|
|
3524
|
+
enforceDiagnosticLogRetention({ force: true });
|
|
3525
|
+
}
|
|
3526
|
+
catch {
|
|
3527
|
+
// Diagnostics cleanup should never bring down the server loop.
|
|
3528
|
+
}
|
|
3529
|
+
}, DIAGNOSTIC_LOG_RETENTION_SWEEP_INTERVAL_MS);
|
|
3530
|
+
diagnosticRetentionTimer.unref?.();
|
|
3520
3531
|
app.addHook("onClose", async () => {
|
|
3532
|
+
clearInterval(diagnosticRetentionTimer);
|
|
3521
3533
|
taskRunWatchdog?.stop();
|
|
3522
3534
|
await managers.backgroundJobs.stop();
|
|
3523
3535
|
});
|
|
3524
|
-
|
|
3536
|
+
const enqueueWikiIngestJob = (jobId) => {
|
|
3537
|
+
managers.backgroundJobs.enqueue({
|
|
3538
|
+
id: jobId,
|
|
3539
|
+
label: `Wiki ingest ${jobId}`,
|
|
3540
|
+
handler: async () => {
|
|
3541
|
+
await processWikiIngestJob(jobId, { llm: managers.llm });
|
|
3542
|
+
}
|
|
3543
|
+
});
|
|
3544
|
+
};
|
|
3545
|
+
for (const pendingJob of listWikiIngestJobs({ limit: 100 })) {
|
|
3546
|
+
if (["queued", "processing"].includes(pendingJob.job.status)) {
|
|
3547
|
+
enqueueWikiIngestJob(pendingJob.job.id);
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
const shouldSkipAutomaticDiagnosticRoute = (url) => {
|
|
3551
|
+
if (!url) {
|
|
3552
|
+
return false;
|
|
3553
|
+
}
|
|
3554
|
+
return (url.startsWith("/api/v1/diagnostics/logs") ||
|
|
3555
|
+
url === "/api/health" ||
|
|
3556
|
+
url === "/api/v1/health" ||
|
|
3557
|
+
url.startsWith("/api/v1/events/meta"));
|
|
3558
|
+
};
|
|
3559
|
+
app.addHook("onRequest", async (request) => {
|
|
3560
|
+
request.diagnosticStartedAt =
|
|
3561
|
+
process.hrtime.bigint();
|
|
3562
|
+
});
|
|
3563
|
+
app.addHook("onResponse", async (request, reply) => {
|
|
3564
|
+
const routeUrl = request.routeOptions.url || request.url;
|
|
3565
|
+
if (shouldSkipAutomaticDiagnosticRoute(routeUrl)) {
|
|
3566
|
+
return;
|
|
3567
|
+
}
|
|
3568
|
+
const startedAt = request.diagnosticStartedAt;
|
|
3569
|
+
const durationMs = typeof startedAt === "bigint"
|
|
3570
|
+
? Number(process.hrtime.bigint() - startedAt) / 1_000_000
|
|
3571
|
+
: null;
|
|
3572
|
+
const source = normalizeDiagnosticSource(request.headers["x-forge-source"]);
|
|
3573
|
+
try {
|
|
3574
|
+
recordDiagnosticLog({
|
|
3575
|
+
level: reply.statusCode >= 500
|
|
3576
|
+
? "error"
|
|
3577
|
+
: reply.statusCode >= 400
|
|
3578
|
+
? "warning"
|
|
3579
|
+
: "debug",
|
|
3580
|
+
source,
|
|
3581
|
+
scope: "api_request",
|
|
3582
|
+
eventKey: `http_${request.method.toLowerCase()}`,
|
|
3583
|
+
message: createDiagnosticMessage({
|
|
3584
|
+
method: request.method,
|
|
3585
|
+
route: routeUrl,
|
|
3586
|
+
statusCode: reply.statusCode
|
|
3587
|
+
}),
|
|
3588
|
+
route: routeUrl,
|
|
3589
|
+
requestId: request.id,
|
|
3590
|
+
details: {
|
|
3591
|
+
method: request.method,
|
|
3592
|
+
rawUrl: request.url,
|
|
3593
|
+
statusCode: reply.statusCode,
|
|
3594
|
+
durationMs: typeof durationMs === "number"
|
|
3595
|
+
? Number(durationMs.toFixed(2))
|
|
3596
|
+
: null,
|
|
3597
|
+
userAgent: typeof request.headers["user-agent"] === "string"
|
|
3598
|
+
? request.headers["user-agent"]
|
|
3599
|
+
: null
|
|
3600
|
+
}
|
|
3601
|
+
});
|
|
3602
|
+
}
|
|
3603
|
+
catch {
|
|
3604
|
+
// Avoid surfacing diagnostics failures as request failures.
|
|
3605
|
+
}
|
|
3606
|
+
});
|
|
3607
|
+
app.setErrorHandler((error, request, reply) => {
|
|
3525
3608
|
const validationIssues = error instanceof ZodError ? formatValidationIssues(error) : undefined;
|
|
3526
3609
|
const statusCode = isHttpError(error)
|
|
3527
3610
|
? error.statusCode
|
|
@@ -3530,6 +3613,38 @@ export async function buildServer(options = {}) {
|
|
|
3530
3613
|
: error instanceof ZodError
|
|
3531
3614
|
? 400
|
|
3532
3615
|
: 500;
|
|
3616
|
+
const routeUrl = request.routeOptions.url || request.url;
|
|
3617
|
+
if (!shouldSkipAutomaticDiagnosticRoute(routeUrl)) {
|
|
3618
|
+
try {
|
|
3619
|
+
recordDiagnosticLog({
|
|
3620
|
+
level: statusCode >= 500 ? "error" : "warning",
|
|
3621
|
+
source: normalizeDiagnosticSource(request.headers["x-forge-source"]),
|
|
3622
|
+
scope: "api_error",
|
|
3623
|
+
eventKey: isHttpError(error)
|
|
3624
|
+
? error.code
|
|
3625
|
+
: isManagerError(error)
|
|
3626
|
+
? error.code
|
|
3627
|
+
: statusCode === 400
|
|
3628
|
+
? "invalid_request"
|
|
3629
|
+
: "internal_error",
|
|
3630
|
+
message: getErrorMessage(error),
|
|
3631
|
+
route: routeUrl,
|
|
3632
|
+
functionName: "setErrorHandler",
|
|
3633
|
+
requestId: request.id,
|
|
3634
|
+
details: {
|
|
3635
|
+
statusCode,
|
|
3636
|
+
validationIssues: validationIssues?.map((issue) => ({
|
|
3637
|
+
path: issue.path,
|
|
3638
|
+
message: issue.message
|
|
3639
|
+
})) ?? [],
|
|
3640
|
+
error: serializeDiagnosticError(error)
|
|
3641
|
+
}
|
|
3642
|
+
});
|
|
3643
|
+
}
|
|
3644
|
+
catch {
|
|
3645
|
+
// Avoid cascading on the error path.
|
|
3646
|
+
}
|
|
3647
|
+
}
|
|
3533
3648
|
reply.code(statusCode).send({
|
|
3534
3649
|
code: isHttpError(error)
|
|
3535
3650
|
? error.code
|
|
@@ -3768,6 +3883,15 @@ export async function buildServer(options = {}) {
|
|
|
3768
3883
|
}
|
|
3769
3884
|
return { session };
|
|
3770
3885
|
});
|
|
3886
|
+
app.post("/api/v1/health/pairing-sessions/revoke-all", async (request) => {
|
|
3887
|
+
const auth = requireOperatorSession(request.headers, {
|
|
3888
|
+
route: "/api/v1/health/pairing-sessions/revoke-all"
|
|
3889
|
+
});
|
|
3890
|
+
return revokeAllCompanionPairingSessions(revokeAllCompanionPairingSessionsSchema.parse(request.body ?? {}), {
|
|
3891
|
+
actor: auth.actor ?? null,
|
|
3892
|
+
source: "ui"
|
|
3893
|
+
});
|
|
3894
|
+
});
|
|
3771
3895
|
app.post("/api/v1/mobile/pairing/verify", async (request) => ({
|
|
3772
3896
|
pairing: verifyCompanionPairing(verifyCompanionPairingSchema.parse(request.body ?? {}))
|
|
3773
3897
|
}));
|
|
@@ -4420,6 +4544,43 @@ export async function buildServer(options = {}) {
|
|
|
4420
4544
|
reply.code(201);
|
|
4421
4545
|
return { profile };
|
|
4422
4546
|
});
|
|
4547
|
+
app.post("/api/v1/wiki/settings/llm-profiles/test", async (request, reply) => {
|
|
4548
|
+
requireScopedAccess(request.headers, ["write"], { route: "/api/v1/wiki/settings/llm-profiles/test" });
|
|
4549
|
+
const parsed = testWikiLlmProfileSchema.parse(request.body ?? {});
|
|
4550
|
+
const existingProfile = parsed.profileId
|
|
4551
|
+
? (listWikiLlmProfiles().find((entry) => entry.id === parsed.profileId) ?? null)
|
|
4552
|
+
: null;
|
|
4553
|
+
const profile = {
|
|
4554
|
+
provider: parsed.provider,
|
|
4555
|
+
baseUrl: parsed.baseUrl,
|
|
4556
|
+
model: parsed.model,
|
|
4557
|
+
systemPrompt: existingProfile?.systemPrompt ?? "",
|
|
4558
|
+
secretId: existingProfile?.secretId ?? null,
|
|
4559
|
+
metadata: {
|
|
4560
|
+
...(existingProfile?.metadata ?? {}),
|
|
4561
|
+
...(parsed.reasoningEffort
|
|
4562
|
+
? { reasoningEffort: parsed.reasoningEffort }
|
|
4563
|
+
: {}),
|
|
4564
|
+
...(parsed.verbosity ? { verbosity: parsed.verbosity } : {})
|
|
4565
|
+
}
|
|
4566
|
+
};
|
|
4567
|
+
const result = await managers.llm.testWikiConnection(profile, parsed.apiKey ?? null, ({ level, message, details = {} }) => {
|
|
4568
|
+
recordDiagnosticLog({
|
|
4569
|
+
level,
|
|
4570
|
+
source: normalizeDiagnosticSource(request.headers["x-forge-source"]),
|
|
4571
|
+
scope: typeof details.scope === "string" ? details.scope : "wiki_llm",
|
|
4572
|
+
eventKey: typeof details.eventKey === "string"
|
|
4573
|
+
? details.eventKey
|
|
4574
|
+
: "llm_connection_test",
|
|
4575
|
+
message,
|
|
4576
|
+
route: "/api/v1/wiki/settings/llm-profiles/test",
|
|
4577
|
+
functionName: "testWikiConnection",
|
|
4578
|
+
details
|
|
4579
|
+
});
|
|
4580
|
+
});
|
|
4581
|
+
reply.code(200);
|
|
4582
|
+
return { result };
|
|
4583
|
+
});
|
|
4423
4584
|
app.post("/api/v1/wiki/settings/embedding-profiles", async (request, reply) => {
|
|
4424
4585
|
requireScopedAccess(request.headers, ["write"], { route: "/api/v1/wiki/settings/embedding-profiles" });
|
|
4425
4586
|
const profile = upsertWikiEmbeddingProfile(upsertWikiEmbeddingProfileSchema.parse(request.body ?? {}), managers.secrets);
|
|
@@ -4553,6 +4714,28 @@ export async function buildServer(options = {}) {
|
|
|
4553
4714
|
const readStringArrayField = (record, key) => Array.isArray(record[key])
|
|
4554
4715
|
? record[key].filter((entry) => typeof entry === "string" && entry.trim().length > 0)
|
|
4555
4716
|
: [];
|
|
4717
|
+
const resolveMappedIngestEntity = (entityType, entityId) => {
|
|
4718
|
+
const result = searchEntities({
|
|
4719
|
+
searches: [
|
|
4720
|
+
{
|
|
4721
|
+
entityTypes: [entityType],
|
|
4722
|
+
ids: [entityId],
|
|
4723
|
+
includeDeleted: false,
|
|
4724
|
+
limit: 1
|
|
4725
|
+
}
|
|
4726
|
+
]
|
|
4727
|
+
}).results[0];
|
|
4728
|
+
if (!result?.ok) {
|
|
4729
|
+
return null;
|
|
4730
|
+
}
|
|
4731
|
+
const match = result.matches?.find((entry) => entry.entityType === entityType && entry.id === entityId);
|
|
4732
|
+
return match
|
|
4733
|
+
? {
|
|
4734
|
+
entityType,
|
|
4735
|
+
entityId
|
|
4736
|
+
}
|
|
4737
|
+
: null;
|
|
4738
|
+
};
|
|
4556
4739
|
const publishIngestProposalEntity = (proposal, auth) => {
|
|
4557
4740
|
const suggestedFields = proposal.suggestedFields &&
|
|
4558
4741
|
typeof proposal.suggestedFields === "object" &&
|
|
@@ -4696,6 +4879,22 @@ export async function buildServer(options = {}) {
|
|
|
4696
4879
|
}, toActivityContext(auth));
|
|
4697
4880
|
return { entityType: "habit", entityId: habit.id };
|
|
4698
4881
|
}
|
|
4882
|
+
case "psyche_value": {
|
|
4883
|
+
const value = createPsycheValue({
|
|
4884
|
+
title,
|
|
4885
|
+
description: summary,
|
|
4886
|
+
valuedDirection: readStringField(suggestedFields, "valuedDirection"),
|
|
4887
|
+
whyItMatters: readStringField(suggestedFields, "whyItMatters"),
|
|
4888
|
+
linkedGoalIds: readStringArrayField(suggestedFields, "linkedGoalIds"),
|
|
4889
|
+
linkedProjectIds: readStringArrayField(suggestedFields, "linkedProjectIds"),
|
|
4890
|
+
linkedTaskIds: readStringArrayField(suggestedFields, "linkedTaskIds"),
|
|
4891
|
+
committedActions: readStringArrayField(suggestedFields, "committedActions"),
|
|
4892
|
+
userId: typeof suggestedFields.userId === "string"
|
|
4893
|
+
? suggestedFields.userId
|
|
4894
|
+
: null
|
|
4895
|
+
}, toActivityContext(auth));
|
|
4896
|
+
return { entityType: "psyche_value", entityId: value.id };
|
|
4897
|
+
}
|
|
4699
4898
|
case "strategy": {
|
|
4700
4899
|
const targetProjectIds = readStringArrayField(suggestedFields, "targetProjectIds");
|
|
4701
4900
|
const linkedEntities = Array.isArray(suggestedFields.linkedEntities) &&
|
|
@@ -4826,13 +5025,7 @@ export async function buildServer(options = {}) {
|
|
|
4826
5025
|
});
|
|
4827
5026
|
const jobId = result.job?.job.id;
|
|
4828
5027
|
if (jobId) {
|
|
4829
|
-
|
|
4830
|
-
id: jobId,
|
|
4831
|
-
label: `Wiki ingest ${jobId}`,
|
|
4832
|
-
handler: async () => {
|
|
4833
|
-
await processWikiIngestJob(jobId, { llm: managers.llm });
|
|
4834
|
-
}
|
|
4835
|
-
});
|
|
5028
|
+
enqueueWikiIngestJob(jobId);
|
|
4836
5029
|
}
|
|
4837
5030
|
reply.code(201);
|
|
4838
5031
|
return result;
|
|
@@ -4849,13 +5042,7 @@ export async function buildServer(options = {}) {
|
|
|
4849
5042
|
});
|
|
4850
5043
|
const jobId = result.job?.job.id;
|
|
4851
5044
|
if (jobId) {
|
|
4852
|
-
|
|
4853
|
-
id: jobId,
|
|
4854
|
-
label: `Wiki ingest ${jobId}`,
|
|
4855
|
-
handler: async () => {
|
|
4856
|
-
await processWikiIngestJob(jobId, { llm: managers.llm });
|
|
4857
|
-
}
|
|
4858
|
-
});
|
|
5045
|
+
enqueueWikiIngestJob(jobId);
|
|
4859
5046
|
}
|
|
4860
5047
|
reply.code(201);
|
|
4861
5048
|
return result;
|
|
@@ -4870,13 +5057,91 @@ export async function buildServer(options = {}) {
|
|
|
4870
5057
|
}
|
|
4871
5058
|
return job;
|
|
4872
5059
|
});
|
|
5060
|
+
app.post("/api/v1/wiki/ingest-jobs/:id/rerun", async (request, reply) => {
|
|
5061
|
+
requireScopedAccess(request.headers, ["write"], { route: "/api/v1/wiki/ingest-jobs/:id/rerun" });
|
|
5062
|
+
const { id } = request.params;
|
|
5063
|
+
try {
|
|
5064
|
+
const result = await rerunWikiIngestJob(id, {
|
|
5065
|
+
actor: requireAuthenticatedActor(request.headers, { route: "/api/v1/wiki/ingest-jobs/:id/rerun" }).actor ?? null
|
|
5066
|
+
});
|
|
5067
|
+
if (!result) {
|
|
5068
|
+
reply.code(404);
|
|
5069
|
+
return { error: "Wiki ingest job not found" };
|
|
5070
|
+
}
|
|
5071
|
+
const nextJobId = result.job?.job.id;
|
|
5072
|
+
if (nextJobId) {
|
|
5073
|
+
enqueueWikiIngestJob(nextJobId);
|
|
5074
|
+
}
|
|
5075
|
+
reply.code(201);
|
|
5076
|
+
return result;
|
|
5077
|
+
}
|
|
5078
|
+
catch (error) {
|
|
5079
|
+
if (error instanceof Error &&
|
|
5080
|
+
error.message.includes("can only be rerun")) {
|
|
5081
|
+
reply.code(409);
|
|
5082
|
+
return { error: error.message };
|
|
5083
|
+
}
|
|
5084
|
+
throw error;
|
|
5085
|
+
}
|
|
5086
|
+
});
|
|
5087
|
+
app.post("/api/v1/wiki/ingest-jobs/:id/resume", async (request, reply) => {
|
|
5088
|
+
requireScopedAccess(request.headers, ["write"], { route: "/api/v1/wiki/ingest-jobs/:id/resume" });
|
|
5089
|
+
const { id } = request.params;
|
|
5090
|
+
const job = getWikiIngestJob(id);
|
|
5091
|
+
if (!job) {
|
|
5092
|
+
reply.code(404);
|
|
5093
|
+
return { error: "Wiki ingest job not found" };
|
|
5094
|
+
}
|
|
5095
|
+
const hasRecoverableOpenAiResponse = job.logs.some((entry) => typeof entry.metadata.responseId === "string") ||
|
|
5096
|
+
job.assets.some((asset) => typeof asset.metadata.openAiResponseId === "string" ||
|
|
5097
|
+
asset.status === "processing");
|
|
5098
|
+
const canResume = ["queued", "processing"].includes(job.job.status) ||
|
|
5099
|
+
(job.job.status === "failed" && hasRecoverableOpenAiResponse);
|
|
5100
|
+
if (!canResume) {
|
|
5101
|
+
reply.code(409);
|
|
5102
|
+
return {
|
|
5103
|
+
error: "Only active wiki ingest jobs, or failed jobs with a recoverable OpenAI background response, can be resumed.",
|
|
5104
|
+
job,
|
|
5105
|
+
resumed: false
|
|
5106
|
+
};
|
|
5107
|
+
}
|
|
5108
|
+
const alreadyActive = managers.backgroundJobs.has(id);
|
|
5109
|
+
if (!alreadyActive) {
|
|
5110
|
+
enqueueWikiIngestJob(id);
|
|
5111
|
+
}
|
|
5112
|
+
return {
|
|
5113
|
+
job: getWikiIngestJob(id),
|
|
5114
|
+
resumed: !alreadyActive
|
|
5115
|
+
};
|
|
5116
|
+
});
|
|
5117
|
+
app.delete("/api/v1/wiki/ingest-jobs/:id", async (request, reply) => {
|
|
5118
|
+
requireScopedAccess(request.headers, ["write"], { route: "/api/v1/wiki/ingest-jobs/:id" });
|
|
5119
|
+
const { id } = request.params;
|
|
5120
|
+
try {
|
|
5121
|
+
const deleted = deleteWikiIngestJob(id);
|
|
5122
|
+
if (!deleted) {
|
|
5123
|
+
reply.code(404);
|
|
5124
|
+
return { error: "Wiki ingest job not found" };
|
|
5125
|
+
}
|
|
5126
|
+
return { deleted };
|
|
5127
|
+
}
|
|
5128
|
+
catch (error) {
|
|
5129
|
+
if (error instanceof Error &&
|
|
5130
|
+
error.message.includes("can only be deleted")) {
|
|
5131
|
+
reply.code(409);
|
|
5132
|
+
return { error: error.message };
|
|
5133
|
+
}
|
|
5134
|
+
throw error;
|
|
5135
|
+
}
|
|
5136
|
+
});
|
|
4873
5137
|
app.post("/api/v1/wiki/ingest-jobs/:id/review", async (request, reply) => {
|
|
4874
5138
|
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/wiki/ingest-jobs/:id/review" });
|
|
4875
5139
|
const { id } = request.params;
|
|
4876
5140
|
const reviewed = await reviewWikiIngestJob(id, reviewWikiIngestJobSchema.parse(request.body ?? {}), {
|
|
4877
5141
|
createNote: (note) => createNote(note, toActivityContext(auth)),
|
|
4878
5142
|
updateNote: (noteId, patch) => updateNote(noteId, patch, toActivityContext(auth)),
|
|
4879
|
-
publishEntity: (proposal) => publishIngestProposalEntity(proposal, auth)
|
|
5143
|
+
publishEntity: (proposal) => publishIngestProposalEntity(proposal, auth),
|
|
5144
|
+
resolveMappedEntity: (entityType, entityId) => resolveMappedIngestEntity(entityType, entityId)
|
|
4880
5145
|
});
|
|
4881
5146
|
if (!reviewed) {
|
|
4882
5147
|
reply.code(404);
|
|
@@ -5504,6 +5769,23 @@ export async function buildServer(options = {}) {
|
|
|
5504
5769
|
reply.code(201);
|
|
5505
5770
|
return event;
|
|
5506
5771
|
});
|
|
5772
|
+
app.post("/api/v1/diagnostics/logs", async (request, reply) => {
|
|
5773
|
+
const payload = createDiagnosticLogSchema.parse(request.body ?? {});
|
|
5774
|
+
const entry = recordDiagnosticLog({
|
|
5775
|
+
...payload,
|
|
5776
|
+
source: payload.source ??
|
|
5777
|
+
normalizeDiagnosticSource(request.headers["x-forge-source"])
|
|
5778
|
+
});
|
|
5779
|
+
reply.code(201);
|
|
5780
|
+
return { log: entry };
|
|
5781
|
+
});
|
|
5782
|
+
app.get("/api/v1/diagnostics/logs", async (request) => {
|
|
5783
|
+
requireOperatorSession(request.headers, {
|
|
5784
|
+
route: "/api/v1/diagnostics/logs"
|
|
5785
|
+
});
|
|
5786
|
+
const query = diagnosticLogListQuerySchema.parse(request.query ?? {});
|
|
5787
|
+
return listDiagnosticLogs(query);
|
|
5788
|
+
});
|
|
5507
5789
|
app.get("/api/v1/events", async (request) => {
|
|
5508
5790
|
const query = eventsListQuerySchema.parse(request.query ?? {});
|
|
5509
5791
|
return { events: listEventLog(query) };
|
package/dist/server/health.js
CHANGED
|
@@ -69,6 +69,10 @@ export const createCompanionPairingSessionSchema = z.object({
|
|
|
69
69
|
"watch-ready"
|
|
70
70
|
])
|
|
71
71
|
});
|
|
72
|
+
export const revokeAllCompanionPairingSessionsSchema = z.object({
|
|
73
|
+
userIds: z.array(z.string().trim().min(1)).default([]),
|
|
74
|
+
includeRevoked: z.boolean().default(false)
|
|
75
|
+
});
|
|
72
76
|
export const mobileHealthSyncSchema = z.object({
|
|
73
77
|
sessionId: z.string().trim().min(1),
|
|
74
78
|
pairingToken: z.string().trim().min(1),
|
|
@@ -445,6 +449,35 @@ function listPairingRows(userIds) {
|
|
|
445
449
|
ORDER BY updated_at DESC, created_at DESC`)
|
|
446
450
|
.all(...params);
|
|
447
451
|
}
|
|
452
|
+
function revokePairingRows(rows, activity) {
|
|
453
|
+
if (rows.length === 0) {
|
|
454
|
+
return [];
|
|
455
|
+
}
|
|
456
|
+
const now = nowIso();
|
|
457
|
+
const reason = activity?.reason ?? "Revoked by operator";
|
|
458
|
+
const revokeStatement = getDatabase().prepare(`UPDATE companion_pairing_sessions
|
|
459
|
+
SET status = 'revoked', last_sync_error = ?, updated_at = ?
|
|
460
|
+
WHERE id = ?`);
|
|
461
|
+
const refetchStatement = getDatabase().prepare(`SELECT * FROM companion_pairing_sessions WHERE id = ?`);
|
|
462
|
+
for (const row of rows) {
|
|
463
|
+
revokeStatement.run(reason, now, row.id);
|
|
464
|
+
recordActivityEvent({
|
|
465
|
+
entityType: "system",
|
|
466
|
+
entityId: row.id,
|
|
467
|
+
eventType: "companion_pairing_revoked",
|
|
468
|
+
title: "Companion pairing revoked",
|
|
469
|
+
description: "An operator revoked a Forge Companion pairing session and blocked further syncs for that device.",
|
|
470
|
+
actor: activity?.actor ?? null,
|
|
471
|
+
source: activity?.source ?? "ui",
|
|
472
|
+
metadata: {
|
|
473
|
+
label: row.label,
|
|
474
|
+
deviceName: row.device_name,
|
|
475
|
+
platform: row.platform
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
return rows.map((row) => mapPairingSession(refetchStatement.get(row.id)));
|
|
480
|
+
}
|
|
448
481
|
function listHealthImportRunRows(userIds, limit = 12) {
|
|
449
482
|
const params = [];
|
|
450
483
|
const where = userIds && userIds.length > 0
|
|
@@ -472,43 +505,53 @@ export function revokeCompanionPairingSession(pairingSessionId, activity) {
|
|
|
472
505
|
if (!current) {
|
|
473
506
|
return undefined;
|
|
474
507
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
.
|
|
481
|
-
|
|
482
|
-
entityType: "system",
|
|
483
|
-
entityId: pairingSessionId,
|
|
484
|
-
eventType: "companion_pairing_revoked",
|
|
485
|
-
title: "Companion pairing revoked",
|
|
486
|
-
description: "An operator revoked a Forge Companion pairing session and blocked further syncs for that device.",
|
|
508
|
+
return revokePairingRows([current], activity)[0];
|
|
509
|
+
}
|
|
510
|
+
export function revokeAllCompanionPairingSessions(input, activity) {
|
|
511
|
+
const parsed = revokeAllCompanionPairingSessionsSchema.parse(input ?? {});
|
|
512
|
+
const rows = listPairingRows(parsed.userIds.length > 0 ? parsed.userIds : undefined)
|
|
513
|
+
.filter((row) => parsed.includeRevoked || row.status !== "revoked");
|
|
514
|
+
const sessions = revokePairingRows(rows, {
|
|
487
515
|
actor: activity?.actor ?? null,
|
|
488
516
|
source: activity?.source ?? "ui",
|
|
489
|
-
|
|
490
|
-
label: current.label,
|
|
491
|
-
deviceName: current.device_name,
|
|
492
|
-
platform: current.platform
|
|
493
|
-
}
|
|
517
|
+
reason: "Revoked by operator (bulk)"
|
|
494
518
|
});
|
|
495
|
-
return
|
|
496
|
-
.
|
|
497
|
-
|
|
519
|
+
return {
|
|
520
|
+
revokedCount: sessions.length,
|
|
521
|
+
sessions
|
|
522
|
+
};
|
|
498
523
|
}
|
|
499
524
|
export function createCompanionPairingSession(baseApiUrl, input) {
|
|
500
525
|
const parsed = createCompanionPairingSessionSchema.parse(input);
|
|
501
526
|
const now = new Date();
|
|
527
|
+
const userId = parsed.userId ?? "user_operator";
|
|
528
|
+
const serializedCapabilities = JSON.stringify(parsed.capabilities);
|
|
502
529
|
const expiresAt = new Date(now.getTime() + parsed.expiresInMinutes * 60_000).toISOString();
|
|
503
530
|
const id = `pair_${randomUUID().replaceAll("-", "").slice(0, 12)}`;
|
|
504
531
|
const pairingToken = randomUUID().replaceAll("-", "");
|
|
532
|
+
const stalePendingRows = getDatabase()
|
|
533
|
+
.prepare(`SELECT *
|
|
534
|
+
FROM companion_pairing_sessions
|
|
535
|
+
WHERE user_id = ?
|
|
536
|
+
AND label = ?
|
|
537
|
+
AND api_base_url = ?
|
|
538
|
+
AND capability_flags_json = ?
|
|
539
|
+
AND status = 'pending'`)
|
|
540
|
+
.all(userId, parsed.label, baseApiUrl, serializedCapabilities);
|
|
541
|
+
if (stalePendingRows.length > 0) {
|
|
542
|
+
revokePairingRows(stalePendingRows, {
|
|
543
|
+
actor: null,
|
|
544
|
+
source: "system",
|
|
545
|
+
reason: "Superseded by a newer pairing QR"
|
|
546
|
+
});
|
|
547
|
+
}
|
|
505
548
|
getDatabase()
|
|
506
549
|
.prepare(`INSERT INTO companion_pairing_sessions (
|
|
507
550
|
id, user_id, label, pairing_token, status, capability_flags_json, api_base_url,
|
|
508
551
|
expires_at, created_at, updated_at
|
|
509
552
|
)
|
|
510
553
|
VALUES (?, ?, ?, ?, 'pending', ?, ?, ?, ?, ?)`)
|
|
511
|
-
.run(id,
|
|
554
|
+
.run(id, userId, parsed.label, pairingToken, serializedCapabilities, baseApiUrl, expiresAt, now.toISOString(), now.toISOString());
|
|
512
555
|
const qrPayload = {
|
|
513
556
|
kind: "forge-companion-pairing",
|
|
514
557
|
apiBaseUrl: baseApiUrl,
|
|
@@ -539,6 +582,24 @@ export function verifyCompanionPairing(payload) {
|
|
|
539
582
|
last_seen_at = ?, paired_at = COALESCE(paired_at, ?), updated_at = ?
|
|
540
583
|
WHERE id = ?`)
|
|
541
584
|
.run(nextStatus, parsed.device.name, parsed.device.platform, parsed.device.appVersion, now, now, now, pairing.id);
|
|
585
|
+
if (parsed.device.name.trim().length > 0) {
|
|
586
|
+
const duplicateRows = getDatabase()
|
|
587
|
+
.prepare(`SELECT *
|
|
588
|
+
FROM companion_pairing_sessions
|
|
589
|
+
WHERE user_id = ?
|
|
590
|
+
AND id != ?
|
|
591
|
+
AND status != 'revoked'
|
|
592
|
+
AND COALESCE(device_name, '') = ?
|
|
593
|
+
AND COALESCE(platform, '') = ?`)
|
|
594
|
+
.all(pairing.user_id, pairing.id, parsed.device.name, parsed.device.platform);
|
|
595
|
+
if (duplicateRows.length > 0) {
|
|
596
|
+
revokePairingRows(duplicateRows, {
|
|
597
|
+
actor: null,
|
|
598
|
+
source: "system",
|
|
599
|
+
reason: "Superseded by a newer verified device pairing"
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
}
|
|
542
603
|
return {
|
|
543
604
|
pairingSession: mapPairingSession(getDatabase()
|
|
544
605
|
.prepare(`SELECT * FROM companion_pairing_sessions WHERE id = ?`)
|