forge-openclaw-plugin 0.2.20 → 0.2.22
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-Ch_xeZ2u.js +63 -0
- package/dist/assets/index-Ch_xeZ2u.js.map +1 -0
- package/dist/assets/index-DvVM7K6j.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 +328 -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 +619 -66
- 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-Ch_xeZ2u.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-DvVM7K6j.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);
|
|
@@ -4527,6 +4688,33 @@ export async function buildServer(options = {}) {
|
|
|
4527
4688
|
}
|
|
4528
4689
|
return getWikiPageDetail(note.id);
|
|
4529
4690
|
});
|
|
4691
|
+
app.delete("/api/v1/wiki/pages/:id", async (request, reply) => {
|
|
4692
|
+
const { id } = request.params;
|
|
4693
|
+
const current = getNoteById(id);
|
|
4694
|
+
if (!current || (current.kind !== "wiki" && current.kind !== "evidence")) {
|
|
4695
|
+
reply.code(404);
|
|
4696
|
+
return { error: "Wiki page not found" };
|
|
4697
|
+
}
|
|
4698
|
+
if (current.slug === "index") {
|
|
4699
|
+
reply.code(400);
|
|
4700
|
+
return { error: "The wiki home page cannot be deleted." };
|
|
4701
|
+
}
|
|
4702
|
+
const linkedEntityType = current.links[0]?.entityType ?? null;
|
|
4703
|
+
const auth = requireNoteAccess(request.headers, linkedEntityType, {
|
|
4704
|
+
route: "/api/v1/wiki/pages/:id",
|
|
4705
|
+
entityType: linkedEntityType
|
|
4706
|
+
});
|
|
4707
|
+
const deleted = deleteEntity("note", id, entityDeleteQuerySchema.parse(request.query ?? {}), toActivityContext(auth));
|
|
4708
|
+
if (!deleted) {
|
|
4709
|
+
reply.code(404);
|
|
4710
|
+
return { error: "Wiki page not found" };
|
|
4711
|
+
}
|
|
4712
|
+
return {
|
|
4713
|
+
deleted: {
|
|
4714
|
+
id: deleted.id
|
|
4715
|
+
}
|
|
4716
|
+
};
|
|
4717
|
+
});
|
|
4530
4718
|
app.post("/api/v1/wiki/search", async (request) => {
|
|
4531
4719
|
requireScopedAccess(request.headers, ["read", "write"], { route: "/api/v1/wiki/search" });
|
|
4532
4720
|
return searchWikiPages(wikiSearchQuerySchema.parse(request.body ?? {}), managers.secrets);
|
|
@@ -4553,6 +4741,28 @@ export async function buildServer(options = {}) {
|
|
|
4553
4741
|
const readStringArrayField = (record, key) => Array.isArray(record[key])
|
|
4554
4742
|
? record[key].filter((entry) => typeof entry === "string" && entry.trim().length > 0)
|
|
4555
4743
|
: [];
|
|
4744
|
+
const resolveMappedIngestEntity = (entityType, entityId) => {
|
|
4745
|
+
const result = searchEntities({
|
|
4746
|
+
searches: [
|
|
4747
|
+
{
|
|
4748
|
+
entityTypes: [entityType],
|
|
4749
|
+
ids: [entityId],
|
|
4750
|
+
includeDeleted: false,
|
|
4751
|
+
limit: 1
|
|
4752
|
+
}
|
|
4753
|
+
]
|
|
4754
|
+
}).results[0];
|
|
4755
|
+
if (!result?.ok) {
|
|
4756
|
+
return null;
|
|
4757
|
+
}
|
|
4758
|
+
const match = result.matches?.find((entry) => entry.entityType === entityType && entry.id === entityId);
|
|
4759
|
+
return match
|
|
4760
|
+
? {
|
|
4761
|
+
entityType,
|
|
4762
|
+
entityId
|
|
4763
|
+
}
|
|
4764
|
+
: null;
|
|
4765
|
+
};
|
|
4556
4766
|
const publishIngestProposalEntity = (proposal, auth) => {
|
|
4557
4767
|
const suggestedFields = proposal.suggestedFields &&
|
|
4558
4768
|
typeof proposal.suggestedFields === "object" &&
|
|
@@ -4696,6 +4906,22 @@ export async function buildServer(options = {}) {
|
|
|
4696
4906
|
}, toActivityContext(auth));
|
|
4697
4907
|
return { entityType: "habit", entityId: habit.id };
|
|
4698
4908
|
}
|
|
4909
|
+
case "psyche_value": {
|
|
4910
|
+
const value = createPsycheValue({
|
|
4911
|
+
title,
|
|
4912
|
+
description: summary,
|
|
4913
|
+
valuedDirection: readStringField(suggestedFields, "valuedDirection"),
|
|
4914
|
+
whyItMatters: readStringField(suggestedFields, "whyItMatters"),
|
|
4915
|
+
linkedGoalIds: readStringArrayField(suggestedFields, "linkedGoalIds"),
|
|
4916
|
+
linkedProjectIds: readStringArrayField(suggestedFields, "linkedProjectIds"),
|
|
4917
|
+
linkedTaskIds: readStringArrayField(suggestedFields, "linkedTaskIds"),
|
|
4918
|
+
committedActions: readStringArrayField(suggestedFields, "committedActions"),
|
|
4919
|
+
userId: typeof suggestedFields.userId === "string"
|
|
4920
|
+
? suggestedFields.userId
|
|
4921
|
+
: null
|
|
4922
|
+
}, toActivityContext(auth));
|
|
4923
|
+
return { entityType: "psyche_value", entityId: value.id };
|
|
4924
|
+
}
|
|
4699
4925
|
case "strategy": {
|
|
4700
4926
|
const targetProjectIds = readStringArrayField(suggestedFields, "targetProjectIds");
|
|
4701
4927
|
const linkedEntities = Array.isArray(suggestedFields.linkedEntities) &&
|
|
@@ -4826,13 +5052,7 @@ export async function buildServer(options = {}) {
|
|
|
4826
5052
|
});
|
|
4827
5053
|
const jobId = result.job?.job.id;
|
|
4828
5054
|
if (jobId) {
|
|
4829
|
-
|
|
4830
|
-
id: jobId,
|
|
4831
|
-
label: `Wiki ingest ${jobId}`,
|
|
4832
|
-
handler: async () => {
|
|
4833
|
-
await processWikiIngestJob(jobId, { llm: managers.llm });
|
|
4834
|
-
}
|
|
4835
|
-
});
|
|
5055
|
+
enqueueWikiIngestJob(jobId);
|
|
4836
5056
|
}
|
|
4837
5057
|
reply.code(201);
|
|
4838
5058
|
return result;
|
|
@@ -4849,13 +5069,7 @@ export async function buildServer(options = {}) {
|
|
|
4849
5069
|
});
|
|
4850
5070
|
const jobId = result.job?.job.id;
|
|
4851
5071
|
if (jobId) {
|
|
4852
|
-
|
|
4853
|
-
id: jobId,
|
|
4854
|
-
label: `Wiki ingest ${jobId}`,
|
|
4855
|
-
handler: async () => {
|
|
4856
|
-
await processWikiIngestJob(jobId, { llm: managers.llm });
|
|
4857
|
-
}
|
|
4858
|
-
});
|
|
5072
|
+
enqueueWikiIngestJob(jobId);
|
|
4859
5073
|
}
|
|
4860
5074
|
reply.code(201);
|
|
4861
5075
|
return result;
|
|
@@ -4870,13 +5084,91 @@ export async function buildServer(options = {}) {
|
|
|
4870
5084
|
}
|
|
4871
5085
|
return job;
|
|
4872
5086
|
});
|
|
5087
|
+
app.post("/api/v1/wiki/ingest-jobs/:id/rerun", async (request, reply) => {
|
|
5088
|
+
requireScopedAccess(request.headers, ["write"], { route: "/api/v1/wiki/ingest-jobs/:id/rerun" });
|
|
5089
|
+
const { id } = request.params;
|
|
5090
|
+
try {
|
|
5091
|
+
const result = await rerunWikiIngestJob(id, {
|
|
5092
|
+
actor: requireAuthenticatedActor(request.headers, { route: "/api/v1/wiki/ingest-jobs/:id/rerun" }).actor ?? null
|
|
5093
|
+
});
|
|
5094
|
+
if (!result) {
|
|
5095
|
+
reply.code(404);
|
|
5096
|
+
return { error: "Wiki ingest job not found" };
|
|
5097
|
+
}
|
|
5098
|
+
const nextJobId = result.job?.job.id;
|
|
5099
|
+
if (nextJobId) {
|
|
5100
|
+
enqueueWikiIngestJob(nextJobId);
|
|
5101
|
+
}
|
|
5102
|
+
reply.code(201);
|
|
5103
|
+
return result;
|
|
5104
|
+
}
|
|
5105
|
+
catch (error) {
|
|
5106
|
+
if (error instanceof Error &&
|
|
5107
|
+
error.message.includes("can only be rerun")) {
|
|
5108
|
+
reply.code(409);
|
|
5109
|
+
return { error: error.message };
|
|
5110
|
+
}
|
|
5111
|
+
throw error;
|
|
5112
|
+
}
|
|
5113
|
+
});
|
|
5114
|
+
app.post("/api/v1/wiki/ingest-jobs/:id/resume", async (request, reply) => {
|
|
5115
|
+
requireScopedAccess(request.headers, ["write"], { route: "/api/v1/wiki/ingest-jobs/:id/resume" });
|
|
5116
|
+
const { id } = request.params;
|
|
5117
|
+
const job = getWikiIngestJob(id);
|
|
5118
|
+
if (!job) {
|
|
5119
|
+
reply.code(404);
|
|
5120
|
+
return { error: "Wiki ingest job not found" };
|
|
5121
|
+
}
|
|
5122
|
+
const hasRecoverableOpenAiResponse = job.logs.some((entry) => typeof entry.metadata.responseId === "string") ||
|
|
5123
|
+
job.assets.some((asset) => typeof asset.metadata.openAiResponseId === "string" ||
|
|
5124
|
+
asset.status === "processing");
|
|
5125
|
+
const canResume = ["queued", "processing"].includes(job.job.status) ||
|
|
5126
|
+
(job.job.status === "failed" && hasRecoverableOpenAiResponse);
|
|
5127
|
+
if (!canResume) {
|
|
5128
|
+
reply.code(409);
|
|
5129
|
+
return {
|
|
5130
|
+
error: "Only active wiki ingest jobs, or failed jobs with a recoverable OpenAI background response, can be resumed.",
|
|
5131
|
+
job,
|
|
5132
|
+
resumed: false
|
|
5133
|
+
};
|
|
5134
|
+
}
|
|
5135
|
+
const alreadyActive = managers.backgroundJobs.has(id);
|
|
5136
|
+
if (!alreadyActive) {
|
|
5137
|
+
enqueueWikiIngestJob(id);
|
|
5138
|
+
}
|
|
5139
|
+
return {
|
|
5140
|
+
job: getWikiIngestJob(id),
|
|
5141
|
+
resumed: !alreadyActive
|
|
5142
|
+
};
|
|
5143
|
+
});
|
|
5144
|
+
app.delete("/api/v1/wiki/ingest-jobs/:id", async (request, reply) => {
|
|
5145
|
+
requireScopedAccess(request.headers, ["write"], { route: "/api/v1/wiki/ingest-jobs/:id" });
|
|
5146
|
+
const { id } = request.params;
|
|
5147
|
+
try {
|
|
5148
|
+
const deleted = deleteWikiIngestJob(id);
|
|
5149
|
+
if (!deleted) {
|
|
5150
|
+
reply.code(404);
|
|
5151
|
+
return { error: "Wiki ingest job not found" };
|
|
5152
|
+
}
|
|
5153
|
+
return { deleted };
|
|
5154
|
+
}
|
|
5155
|
+
catch (error) {
|
|
5156
|
+
if (error instanceof Error &&
|
|
5157
|
+
error.message.includes("can only be deleted")) {
|
|
5158
|
+
reply.code(409);
|
|
5159
|
+
return { error: error.message };
|
|
5160
|
+
}
|
|
5161
|
+
throw error;
|
|
5162
|
+
}
|
|
5163
|
+
});
|
|
4873
5164
|
app.post("/api/v1/wiki/ingest-jobs/:id/review", async (request, reply) => {
|
|
4874
5165
|
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/wiki/ingest-jobs/:id/review" });
|
|
4875
5166
|
const { id } = request.params;
|
|
4876
5167
|
const reviewed = await reviewWikiIngestJob(id, reviewWikiIngestJobSchema.parse(request.body ?? {}), {
|
|
4877
5168
|
createNote: (note) => createNote(note, toActivityContext(auth)),
|
|
4878
5169
|
updateNote: (noteId, patch) => updateNote(noteId, patch, toActivityContext(auth)),
|
|
4879
|
-
publishEntity: (proposal) => publishIngestProposalEntity(proposal, auth)
|
|
5170
|
+
publishEntity: (proposal) => publishIngestProposalEntity(proposal, auth),
|
|
5171
|
+
resolveMappedEntity: (entityType, entityId) => resolveMappedIngestEntity(entityType, entityId)
|
|
4880
5172
|
});
|
|
4881
5173
|
if (!reviewed) {
|
|
4882
5174
|
reply.code(404);
|
|
@@ -5504,6 +5796,23 @@ export async function buildServer(options = {}) {
|
|
|
5504
5796
|
reply.code(201);
|
|
5505
5797
|
return event;
|
|
5506
5798
|
});
|
|
5799
|
+
app.post("/api/v1/diagnostics/logs", async (request, reply) => {
|
|
5800
|
+
const payload = createDiagnosticLogSchema.parse(request.body ?? {});
|
|
5801
|
+
const entry = recordDiagnosticLog({
|
|
5802
|
+
...payload,
|
|
5803
|
+
source: payload.source ??
|
|
5804
|
+
normalizeDiagnosticSource(request.headers["x-forge-source"])
|
|
5805
|
+
});
|
|
5806
|
+
reply.code(201);
|
|
5807
|
+
return { log: entry };
|
|
5808
|
+
});
|
|
5809
|
+
app.get("/api/v1/diagnostics/logs", async (request) => {
|
|
5810
|
+
requireOperatorSession(request.headers, {
|
|
5811
|
+
route: "/api/v1/diagnostics/logs"
|
|
5812
|
+
});
|
|
5813
|
+
const query = diagnosticLogListQuerySchema.parse(request.query ?? {});
|
|
5814
|
+
return listDiagnosticLogs(query);
|
|
5815
|
+
});
|
|
5507
5816
|
app.get("/api/v1/events", async (request) => {
|
|
5508
5817
|
const query = eventsListQuerySchema.parse(request.query ?? {});
|
|
5509
5818
|
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 = ?`)
|