forge-openclaw-plugin 0.2.42 → 0.2.44
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 +6 -0
- package/dist/assets/{board-C3BJqvZu.js → board-CAszQU7Y.js} +2 -2
- package/dist/assets/{board-C3BJqvZu.js.map → board-CAszQU7Y.js.map} +1 -1
- package/dist/assets/index-DtEvFzXp.css +1 -0
- package/dist/assets/index-s24CefIb.js +91 -0
- package/dist/assets/index-s24CefIb.js.map +1 -0
- package/dist/assets/{motion-Cv9HdOn4.js → motion-CU5aNClV.js} +2 -2
- package/dist/assets/{motion-Cv9HdOn4.js.map → motion-CU5aNClV.js.map} +1 -1
- package/dist/assets/{table-B1MDOEFp.js → table-CK0KcPYW.js} +2 -2
- package/dist/assets/{table-B1MDOEFp.js.map → table-CK0KcPYW.js.map} +1 -1
- package/dist/assets/{ui-CQzq3TE5.js → ui-B5MjRjKe.js} +2 -2
- package/dist/assets/{ui-CQzq3TE5.js.map → ui-B5MjRjKe.js.map} +1 -1
- package/dist/assets/{vendor-DK-mJFy6.js → vendor-D_NZFJze.js} +2 -2
- package/dist/assets/{vendor-DK-mJFy6.js.map → vendor-D_NZFJze.js.map} +1 -1
- package/dist/index.html +7 -7
- package/dist/openclaw/tools.js +42 -10
- package/dist/server/server/src/app.js +151 -34
- package/dist/server/server/src/health.js +49 -0
- package/dist/server/server/src/movement.js +2 -3
- package/dist/server/server/src/openapi.js +19 -1
- package/dist/server/src/components/ui/info-tooltip.js +1 -1
- package/dist/server/src/lib/knowledge-graph.js +2 -2
- package/dist/server/src/lib/schemas.js +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +43 -2
- package/skills/forge-openclaw/entity_conversation_playbooks.md +23 -4
- package/skills/forge-openclaw/psyche_entity_playbooks.md +8 -0
- package/dist/assets/index-Dj8vB0vh.css +0 -1
- package/dist/assets/index-Fe5y4VWU.js +0 -91
- package/dist/assets/index-Fe5y4VWU.js.map +0 -1
package/dist/index.html
CHANGED
|
@@ -13,14 +13,14 @@
|
|
|
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/vendor-
|
|
18
|
-
<link rel="modulepreload" crossorigin href="/forge/assets/board-
|
|
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-
|
|
16
|
+
<script type="module" crossorigin src="/forge/assets/index-s24CefIb.js"></script>
|
|
17
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/vendor-D_NZFJze.js">
|
|
18
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/board-CAszQU7Y.js">
|
|
19
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/ui-B5MjRjKe.js">
|
|
20
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/motion-CU5aNClV.js">
|
|
21
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/table-CK0KcPYW.js">
|
|
22
22
|
<link rel="stylesheet" crossorigin href="/forge/assets/vendor-DT3pnAKJ.css">
|
|
23
|
-
<link rel="stylesheet" crossorigin href="/forge/assets/index-
|
|
23
|
+
<link rel="stylesheet" crossorigin href="/forge/assets/index-DtEvFzXp.css">
|
|
24
24
|
</head>
|
|
25
25
|
<body class="bg-canvas text-ink antialiased">
|
|
26
26
|
<div id="root"></div>
|
package/dist/openclaw/tools.js
CHANGED
|
@@ -11,6 +11,46 @@ function jsonResult(payload) {
|
|
|
11
11
|
details: payload
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
|
+
function normalizeText(value) {
|
|
15
|
+
return typeof value === "string" ? value.trim() : "";
|
|
16
|
+
}
|
|
17
|
+
function normalizeOptionalText(value) {
|
|
18
|
+
const text = normalizeText(value);
|
|
19
|
+
return text.length > 0 ? text : undefined;
|
|
20
|
+
}
|
|
21
|
+
function normalizeTaskRunStartRequest(params) {
|
|
22
|
+
const taskId = normalizeText(params.taskId);
|
|
23
|
+
if (!taskId) {
|
|
24
|
+
throw new Error("forge_start_task_run requires a non-empty taskId.");
|
|
25
|
+
}
|
|
26
|
+
const actor = normalizeText(params.actor);
|
|
27
|
+
if (!actor) {
|
|
28
|
+
throw new Error("forge_start_task_run requires a non-empty actor.");
|
|
29
|
+
}
|
|
30
|
+
const timerMode = params.timerMode === "planned" ? "planned" : "unlimited";
|
|
31
|
+
const plannedDurationSeconds = typeof params.plannedDurationSeconds === "number" &&
|
|
32
|
+
Number.isInteger(params.plannedDurationSeconds)
|
|
33
|
+
? params.plannedDurationSeconds
|
|
34
|
+
: null;
|
|
35
|
+
if (timerMode === "planned" && plannedDurationSeconds === null) {
|
|
36
|
+
throw new Error("forge_start_task_run requires plannedDurationSeconds when timerMode is planned.");
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
taskId,
|
|
40
|
+
body: {
|
|
41
|
+
actor,
|
|
42
|
+
timerMode,
|
|
43
|
+
plannedDurationSeconds: timerMode === "planned" ? plannedDurationSeconds : null,
|
|
44
|
+
overrideReason: normalizeOptionalText(params.overrideReason),
|
|
45
|
+
isCurrent: typeof params.isCurrent === "boolean" ? params.isCurrent : undefined,
|
|
46
|
+
leaseTtlSeconds: typeof params.leaseTtlSeconds === "number" &&
|
|
47
|
+
Number.isInteger(params.leaseTtlSeconds)
|
|
48
|
+
? params.leaseTtlSeconds
|
|
49
|
+
: undefined,
|
|
50
|
+
note: normalizeOptionalText(params.note)
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
14
54
|
async function runRead(config, path) {
|
|
15
55
|
const result = await callConfiguredForgeApi(config, {
|
|
16
56
|
method: "GET",
|
|
@@ -954,19 +994,11 @@ export function registerForgePluginTools(api, config) {
|
|
|
954
994
|
note: Type.Optional(Type.String())
|
|
955
995
|
}),
|
|
956
996
|
async execute(_toolCallId, params) {
|
|
957
|
-
const typed = params;
|
|
997
|
+
const typed = normalizeTaskRunStartRequest(params);
|
|
958
998
|
return jsonResult(await runWrite(config, {
|
|
959
999
|
method: "POST",
|
|
960
1000
|
path: `/api/v1/tasks/${typed.taskId}/runs`,
|
|
961
|
-
body:
|
|
962
|
-
actor: typed.actor,
|
|
963
|
-
timerMode: typed.timerMode,
|
|
964
|
-
plannedDurationSeconds: typed.plannedDurationSeconds,
|
|
965
|
-
overrideReason: typed.overrideReason,
|
|
966
|
-
isCurrent: typed.isCurrent,
|
|
967
|
-
leaseTtlSeconds: typed.leaseTtlSeconds,
|
|
968
|
-
note: typed.note
|
|
969
|
-
}
|
|
1001
|
+
body: typed.body
|
|
970
1002
|
}));
|
|
971
1003
|
}
|
|
972
1004
|
});
|
|
@@ -61,7 +61,7 @@ import { buildOpenApiDocument } from "./openapi.js";
|
|
|
61
61
|
import { registerWebRoutes } from "./web.js";
|
|
62
62
|
import { createManagerRuntime } from "./managers/runtime.js";
|
|
63
63
|
import { isManagerError } from "./managers/type-guards.js";
|
|
64
|
-
import { createCompanionPairingSession, createCompanionPairingSessionSchema, createSleepSession, createSleepSessionSchema, createWorkoutSession, createWorkoutSessionSchema, deleteSleepSession, deleteWorkoutSession, getCompanionPairingSessionById, getCompanionOverview, getFitnessViewData, getSleepSessionById, getSleepSessionDetailById, getSleepViewData, getVitalsViewData, getWorkoutSessionById, ingestMobileHealthSync, mobileHealthSyncSchema, patchCompanionPairingSourceState, patchCompanionPairingSourceStateSchema, companionSourceKeySchema, requireValidPairing, revokeAllCompanionPairingSessions, revokeAllCompanionPairingSessionsSchema, revokeCompanionPairingSession, updateMobileCompanionSourceState, updateMobileCompanionSourceStateSchema, verifyCompanionPairing, verifyCompanionPairingSchema, updateSleepMetadata, updateSleepMetadataSchema, updateWorkoutMetadata, updateWorkoutMetadataSchema } from "./health.js";
|
|
64
|
+
import { createCompanionPairingSession, createCompanionPairingSessionSchema, createSleepSession, createSleepSessionSchema, createWorkoutSession, createWorkoutSessionSchema, deleteSleepSession, deleteWorkoutSession, getCompanionPairingSessionById, getCompanionOverview, getFitnessViewData, getSleepSessionById, getSleepSessionDetailById, getSleepTimelineOverlaysForRange, getSleepViewData, getVitalsViewData, getWorkoutSessionById, ingestMobileHealthSync, mobileHealthSyncSchema, patchCompanionPairingSourceState, patchCompanionPairingSourceStateSchema, companionSourceKeySchema, requireValidPairing, revokeAllCompanionPairingSessions, revokeAllCompanionPairingSessionsSchema, revokeCompanionPairingSession, updateMobileCompanionSourceState, updateMobileCompanionSourceStateSchema, verifyCompanionPairing, verifyCompanionPairingSchema, updateSleepMetadata, updateSleepMetadataSchema, updateWorkoutMetadata, updateWorkoutMetadataSchema } from "./health.js";
|
|
65
65
|
import { analyzeMovementUserBoxPreflight, createMovementUserBox, createMovementPlace, deleteMovementUserBox, getMovementAllTimeSummary, getMovementBoxDetail, getMovementDayDetail, getMovementMobileBootstrap, getMovementTimeline, getMovementSelectionAggregate, getMovementSettings, getMovementTripDetail, getMovementMonthSummary, invalidateAutomaticMovementBox, listMovementPlaces, movementAutomaticBoxInvalidateSchema, movementMobileBootstrapSchema, movementMobilePlaceMutationSchema, movementMobileStayPatchSchema, movementMobileUserBoxCreateSchema, movementMobileUserBoxPreflightSchema, movementMobileUserBoxPatchSchema, movementMobileAutomaticBoxInvalidateSchema, movementMobileTimelineSchema, movementPlaceMutationSchema, movementPlacePatchSchema, movementSelectionAggregateSchema, movementStayPatchSchema, movementTripPatchSchema, movementUserBoxCreateSchema, movementUserBoxPreflightSchema, movementUserBoxPatchSchema, movementSettingsPatchSchema, movementTimelineQuerySchema, movementTripPointPatchSchema, deleteMovementStay, deleteMovementTrip, deleteMovementTripPoint, updateMovementPlace, updateMovementSettings, updateMovementStay, updateMovementTrip, updateMovementUserBox, updateMovementTripPoint, resolveMovementTimelineSegmentForBox } from "./movement.js";
|
|
66
66
|
import { getScreenTimeAllTimeSummary, getScreenTimeDayDetail, getScreenTimeMonthSummary, getScreenTimeSettings, screenTimeSettingsPatchSchema, updateScreenTimeSettings } from "./screen-time.js";
|
|
67
67
|
import { assertWatchReady, buildWatchBootstrap, ingestWatchCaptureBatch, mobileWatchBootstrapSchema, mobileWatchCaptureBatchSchema, mobileWatchHabitCheckInSchema } from "./watch-mobile.js";
|
|
@@ -1980,6 +1980,29 @@ function buildPreferredMutationPath(entityType) {
|
|
|
1980
1980
|
return "Read-only surface.";
|
|
1981
1981
|
}
|
|
1982
1982
|
}
|
|
1983
|
+
function buildPreferredMutationTool(entityType) {
|
|
1984
|
+
if (entityType in AGENT_ONBOARDING_BATCH_ROUTE_BASES) {
|
|
1985
|
+
return "forge_create_entities | forge_update_entities | forge_delete_entities | forge_search_entities";
|
|
1986
|
+
}
|
|
1987
|
+
switch (entityType) {
|
|
1988
|
+
case "wiki_page":
|
|
1989
|
+
return "forge_upsert_wiki_page";
|
|
1990
|
+
case "calendar_connection":
|
|
1991
|
+
return "forge_connect_calendar_provider | forge_sync_calendar_connection";
|
|
1992
|
+
case "task_run":
|
|
1993
|
+
return "forge_start_task_run | forge_heartbeat_task_run | forge_focus_task_run | forge_complete_task_run | forge_release_task_run";
|
|
1994
|
+
case "questionnaire_run":
|
|
1995
|
+
return "forge_start_questionnaire_run | forge_update_questionnaire_run | forge_complete_questionnaire_run";
|
|
1996
|
+
case "preference_judgment":
|
|
1997
|
+
return "forge_submit_preferences_judgment";
|
|
1998
|
+
case "preference_signal":
|
|
1999
|
+
return "forge_submit_preferences_signal";
|
|
2000
|
+
case "work_adjustment":
|
|
2001
|
+
return "forge_adjust_work_minutes";
|
|
2002
|
+
default:
|
|
2003
|
+
return null;
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
1983
2006
|
function buildPreferredReadPath(entityType) {
|
|
1984
2007
|
if (entityType in AGENT_ONBOARDING_BATCH_ROUTE_BASES) {
|
|
1985
2008
|
return AGENT_ONBOARDING_BATCH_ROUTE_BASES[entityType];
|
|
@@ -2024,11 +2047,10 @@ function enrichOnboardingEntityGuide(entry) {
|
|
|
2024
2047
|
: null,
|
|
2025
2048
|
preferredMutationPath: buildPreferredMutationPath(entry.entityType),
|
|
2026
2049
|
preferredReadPath: buildPreferredReadPath(entry.entityType),
|
|
2027
|
-
preferredMutationTool:
|
|
2028
|
-
|
|
2029
|
-
: classification === "specialized_domain_surface"
|
|
2050
|
+
preferredMutationTool: buildPreferredMutationTool(entry.entityType) ??
|
|
2051
|
+
(classification === "specialized_domain_surface"
|
|
2030
2052
|
? "Follow forge_get_agent_onboarding.entityRouteModel.specializedDomainSurfaces for the dedicated route family."
|
|
2031
|
-
: null
|
|
2053
|
+
: null)
|
|
2032
2054
|
};
|
|
2033
2055
|
}
|
|
2034
2056
|
const AGENT_ONBOARDING_ENTITY_CATALOG = [
|
|
@@ -2758,7 +2780,8 @@ const AGENT_ONBOARDING_CONVERSATION_RULES = [
|
|
|
2758
2780
|
"For specialized surfaces, ask what would make the answer or change useful before you ask route-shaped details such as provider, weekday, flow id, run id, or trip id.",
|
|
2759
2781
|
"When the user already named a precise correction or review target, do not widen back out into a meta lane question. Confirm only the missing route-selecting detail and then act.",
|
|
2760
2782
|
"Once the route family is clear, say it plainly enough that another agent could follow the same path without guessing.",
|
|
2761
|
-
"For Movement specifically, treat missing-data corrections as user-defined overlay boxes unless the user is editing an already-recorded stay or trip. When the user already gave a clear instruction like 'that missing block was home', act after only the last ambiguity is resolved."
|
|
2783
|
+
"For Movement specifically, treat missing-data corrections as user-defined overlay boxes unless the user is editing an already-recorded stay or trip. When the user already gave a clear instruction like 'that missing block was home', act after only the last ambiguity is resolved.",
|
|
2784
|
+
"For meaning-bearing updates, especially in Psyche, briefly say what feels newly true before you ask for the one structural detail that still changes the save."
|
|
2762
2785
|
];
|
|
2763
2786
|
const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
|
|
2764
2787
|
{
|
|
@@ -2917,7 +2940,8 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
|
|
|
2917
2940
|
"Confirm the task.",
|
|
2918
2941
|
"Confirm the actor only if it is not already obvious.",
|
|
2919
2942
|
"Ask whether the run should be planned or unlimited only if that changes the action.",
|
|
2920
|
-
"Start the run instead of turning it into a longer intake."
|
|
2943
|
+
"Start the run instead of turning it into a longer intake.",
|
|
2944
|
+
"Use the dedicated task-run tool for start, heartbeat, focus, complete, and release work instead of bouncing to the UI or a generic web route."
|
|
2921
2945
|
]
|
|
2922
2946
|
},
|
|
2923
2947
|
{
|
|
@@ -3071,6 +3095,7 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
|
|
|
3071
3095
|
"Ask whether the focus is a stay, a trip, a place, a timeline window, or a selected span.",
|
|
3072
3096
|
"Ask for the time window, place, or movement item that makes the question concrete.",
|
|
3073
3097
|
"Ask what they are trying to notice, preserve, or answer through that movement context.",
|
|
3098
|
+
"Choose the dedicated day, month, all-time, timeline, places, trip-detail, or selection route once the question shape is clear.",
|
|
3074
3099
|
"Skip the meta lane question when the user already named the exact correction or review target and only one ambiguity remains.",
|
|
3075
3100
|
"If the request is filling a missing-data gap, use a user-defined movement box rather than a raw stay or trip patch.",
|
|
3076
3101
|
"If the request is repairing already-saved movement data, use the repair route that matches the saved object instead of treating it like a missing span.",
|
|
@@ -3104,6 +3129,7 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
|
|
|
3104
3129
|
"Ask whether they need the flow contract, a run result, a published output, or a node result.",
|
|
3105
3130
|
"Ask what the user is trying to learn, repair, or publish through that flow.",
|
|
3106
3131
|
"If the user already named the flow and action clearly, skip the meta lane question and ask only for the missing run, node, or output scope.",
|
|
3132
|
+
"If the user wants a stable public input contract or published output, prefer those dedicated reads instead of detouring through run history first.",
|
|
3107
3133
|
"If the user is still shaping a payload or edit, prefer flow detail or box catalog reads before asking for structured inputs.",
|
|
3108
3134
|
"Prefer flow detail or published-output reads for stable contracts, and use run or node-result routes only when the user is asking about execution history or debugging.",
|
|
3109
3135
|
"Route to the dedicated workbench route family once the execution lane is clear."
|
|
@@ -3853,6 +3879,61 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
|
|
|
3853
3879
|
example: '{"taskRunId":"run_123","actor":"aurel","note":"Stopping for now; blocked on feedback","closeoutNote":{"contentMarkdown":"Blocked on feedback from design before I can continue."}}'
|
|
3854
3880
|
}
|
|
3855
3881
|
];
|
|
3882
|
+
const VALIDATION_ROUTE_TOOL_MAP = {
|
|
3883
|
+
"POST /api/v1/entities/search": "forge_search_entities",
|
|
3884
|
+
"POST /api/v1/entities/create": "forge_create_entities",
|
|
3885
|
+
"POST /api/v1/entities/update": "forge_update_entities",
|
|
3886
|
+
"POST /api/v1/entities/delete": "forge_delete_entities",
|
|
3887
|
+
"POST /api/v1/entities/restore": "forge_restore_entities",
|
|
3888
|
+
"POST /api/v1/tasks/:id/runs": "forge_start_task_run",
|
|
3889
|
+
"POST /api/v1/task-runs/:id/heartbeat": "forge_heartbeat_task_run",
|
|
3890
|
+
"POST /api/v1/task-runs/:id/focus": "forge_focus_task_run",
|
|
3891
|
+
"POST /api/v1/task-runs/:id/complete": "forge_complete_task_run",
|
|
3892
|
+
"POST /api/v1/task-runs/:id/release": "forge_release_task_run",
|
|
3893
|
+
"POST /api/tasks/:id/runs": "forge_start_task_run",
|
|
3894
|
+
"POST /api/task-runs/:id/heartbeat": "forge_heartbeat_task_run",
|
|
3895
|
+
"POST /api/task-runs/:id/focus": "forge_focus_task_run",
|
|
3896
|
+
"POST /api/task-runs/:id/complete": "forge_complete_task_run",
|
|
3897
|
+
"POST /api/task-runs/:id/release": "forge_release_task_run"
|
|
3898
|
+
};
|
|
3899
|
+
function formatValidationSummary(issues) {
|
|
3900
|
+
return issues
|
|
3901
|
+
.slice(0, 3)
|
|
3902
|
+
.map((issue) => `${issue.path}: ${issue.message}`)
|
|
3903
|
+
.join("; ");
|
|
3904
|
+
}
|
|
3905
|
+
function buildValidationHelp(method, routeUrl, issues) {
|
|
3906
|
+
const route = routeUrl || "unknown_route";
|
|
3907
|
+
const toolName = VALIDATION_ROUTE_TOOL_MAP[`${method.toUpperCase()} ${route}`];
|
|
3908
|
+
const toolInput = toolName
|
|
3909
|
+
? AGENT_ONBOARDING_TOOL_INPUT_CATALOG.find((entry) => entry.toolName === toolName)
|
|
3910
|
+
: undefined;
|
|
3911
|
+
return {
|
|
3912
|
+
route,
|
|
3913
|
+
validationSummary: formatValidationSummary(issues),
|
|
3914
|
+
...(toolInput
|
|
3915
|
+
? {
|
|
3916
|
+
expectedShape: {
|
|
3917
|
+
toolName: toolInput.toolName,
|
|
3918
|
+
inputShape: toolInput.inputShape,
|
|
3919
|
+
requiredFields: [...toolInput.requiredFields],
|
|
3920
|
+
example: toolInput.example,
|
|
3921
|
+
notes: [...toolInput.notes]
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
: {
|
|
3925
|
+
expectedShape: {
|
|
3926
|
+
inputShape: "See details[] for the rejected fields on this route.",
|
|
3927
|
+
requiredFields: [],
|
|
3928
|
+
example: null,
|
|
3929
|
+
notes: [
|
|
3930
|
+
"Retry with only the documented top-level keys for this route.",
|
|
3931
|
+
"Omit optional fields instead of sending null unless the route contract explicitly allows null."
|
|
3932
|
+
]
|
|
3933
|
+
}
|
|
3934
|
+
})
|
|
3935
|
+
};
|
|
3936
|
+
}
|
|
3856
3937
|
function buildAgentOnboardingPayload(request) {
|
|
3857
3938
|
const origin = getRequestOrigin(request);
|
|
3858
3939
|
return {
|
|
@@ -4198,10 +4279,12 @@ function buildAgentOnboardingPayload(request) {
|
|
|
4198
4279
|
verifyCommands: [
|
|
4199
4280
|
`curl -s ${origin}/api/v1/health`,
|
|
4200
4281
|
"openclaw plugins install ./projects/forge/openclaw-plugin",
|
|
4282
|
+
"openclaw plugins info forge-openclaw-plugin",
|
|
4201
4283
|
"openclaw gateway restart"
|
|
4202
4284
|
],
|
|
4203
4285
|
configNotes: [
|
|
4204
4286
|
"Localhost and Tailscale targets can usually use the operator-session path without a long-lived token.",
|
|
4287
|
+
"If your current OpenClaw build blocks the repo-local install because of the package scanner, keep the repo folder on plugins.load.paths and verify that plugins info still points at the local Forge source path before continuing.",
|
|
4205
4288
|
"Use a distinct actor label such as Albert (claw) so OpenClaw-originated work stays obvious in Forge provenance.",
|
|
4206
4289
|
"Create each agent as a Forge bot user, then use userId or userIds in tool inputs whenever the agent should focus on one human, one bot, or a specific collaboration slice."
|
|
4207
4290
|
]
|
|
@@ -4558,6 +4641,30 @@ function resolveScopedUserIds(query) {
|
|
|
4558
4641
|
const unique = Array.from(new Set(values));
|
|
4559
4642
|
return unique.length > 0 ? unique : undefined;
|
|
4560
4643
|
}
|
|
4644
|
+
function attachMovementTimelineSleepOverlays(movement, userIds) {
|
|
4645
|
+
const rangeStart = movement.segments.reduce((earliest, segment) => {
|
|
4646
|
+
if (!earliest || Date.parse(segment.startedAt) < Date.parse(earliest)) {
|
|
4647
|
+
return segment.startedAt;
|
|
4648
|
+
}
|
|
4649
|
+
return earliest;
|
|
4650
|
+
}, null);
|
|
4651
|
+
const rangeEnd = movement.segments.reduce((latest, segment) => {
|
|
4652
|
+
if (!latest || Date.parse(segment.endedAt) > Date.parse(latest)) {
|
|
4653
|
+
return segment.endedAt;
|
|
4654
|
+
}
|
|
4655
|
+
return latest;
|
|
4656
|
+
}, null);
|
|
4657
|
+
return {
|
|
4658
|
+
...movement,
|
|
4659
|
+
sleepOverlays: rangeStart && rangeEnd
|
|
4660
|
+
? getSleepTimelineOverlaysForRange({
|
|
4661
|
+
startedAt: rangeStart,
|
|
4662
|
+
endedAt: rangeEnd,
|
|
4663
|
+
userIds
|
|
4664
|
+
})
|
|
4665
|
+
: []
|
|
4666
|
+
};
|
|
4667
|
+
}
|
|
4561
4668
|
function readRequestedUserIdFromBody(body) {
|
|
4562
4669
|
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
4563
4670
|
return undefined;
|
|
@@ -4581,13 +4688,17 @@ function syncEntityOwnerFromBody(options) {
|
|
|
4581
4688
|
setEntityOwner(options.entityType, options.entityId, owner.id);
|
|
4582
4689
|
}
|
|
4583
4690
|
function buildV1Context(userIds) {
|
|
4584
|
-
const goals = filterOwnedEntities("goal", listGoals(), userIds);
|
|
4585
|
-
const tasks = filterOwnedEntities("task", listTasks(), userIds);
|
|
4586
|
-
const habits = filterOwnedEntities("habit", listHabits(), userIds);
|
|
4587
4691
|
const users = listUsers();
|
|
4588
|
-
const
|
|
4589
|
-
const
|
|
4590
|
-
?
|
|
4692
|
+
const validUserIdSet = new Set(users.map((user) => user.id));
|
|
4693
|
+
const scopedUserIds = userIds && userIds.length > 0
|
|
4694
|
+
? userIds.filter((userId) => validUserIdSet.has(userId))
|
|
4695
|
+
: undefined;
|
|
4696
|
+
const goals = filterOwnedEntities("goal", listGoals(), scopedUserIds);
|
|
4697
|
+
const tasks = filterOwnedEntities("task", listTasks(), scopedUserIds);
|
|
4698
|
+
const habits = filterOwnedEntities("habit", listHabits(), scopedUserIds);
|
|
4699
|
+
const dashboard = getDashboard({ userIds: scopedUserIds });
|
|
4700
|
+
const selectedUsers = scopedUserIds && scopedUserIds.length > 0
|
|
4701
|
+
? users.filter((user) => scopedUserIds.includes(user.id))
|
|
4591
4702
|
: users;
|
|
4592
4703
|
return {
|
|
4593
4704
|
meta: {
|
|
@@ -4599,23 +4710,23 @@ function buildV1Context(userIds) {
|
|
|
4599
4710
|
},
|
|
4600
4711
|
metrics: buildGamificationProfile(goals, tasks, habits),
|
|
4601
4712
|
dashboard,
|
|
4602
|
-
overview: getOverviewContext(new Date(), { userIds }),
|
|
4603
|
-
today: getTodayContext(new Date(), { userIds }),
|
|
4604
|
-
risk: getRiskContext(new Date(), { userIds }),
|
|
4713
|
+
overview: getOverviewContext(new Date(), { userIds: scopedUserIds }),
|
|
4714
|
+
today: getTodayContext(new Date(), { userIds: scopedUserIds }),
|
|
4715
|
+
risk: getRiskContext(new Date(), { userIds: scopedUserIds }),
|
|
4605
4716
|
goals,
|
|
4606
|
-
projects: listProjectSummaries({ userIds }),
|
|
4717
|
+
projects: listProjectSummaries({ userIds: scopedUserIds }),
|
|
4607
4718
|
tags: listTags(),
|
|
4608
4719
|
tasks,
|
|
4609
4720
|
habits,
|
|
4610
4721
|
users,
|
|
4611
|
-
strategies: listStrategies({ userIds }),
|
|
4722
|
+
strategies: listStrategies({ userIds: scopedUserIds }),
|
|
4612
4723
|
userScope: {
|
|
4613
|
-
selectedUserIds:
|
|
4724
|
+
selectedUserIds: scopedUserIds ?? [],
|
|
4614
4725
|
selectedUsers
|
|
4615
4726
|
},
|
|
4616
4727
|
activeTaskRuns: listTaskRuns({ active: true, limit: 25 }),
|
|
4617
4728
|
activity: dashboard.recentActivity,
|
|
4618
|
-
lifeForce: buildLifeForcePayload(new Date(),
|
|
4729
|
+
lifeForce: buildLifeForcePayload(new Date(), scopedUserIds)
|
|
4619
4730
|
};
|
|
4620
4731
|
}
|
|
4621
4732
|
function buildXpMetricsPayload() {
|
|
@@ -5045,6 +5156,10 @@ export async function buildServer(options = {}) {
|
|
|
5045
5156
|
});
|
|
5046
5157
|
app.setErrorHandler((error, request, reply) => {
|
|
5047
5158
|
const validationIssues = error instanceof ZodError ? formatValidationIssues(error) : undefined;
|
|
5159
|
+
const routeUrl = request.routeOptions.url || request.url;
|
|
5160
|
+
const validationHelp = validationIssues
|
|
5161
|
+
? buildValidationHelp(request.method, routeUrl, validationIssues)
|
|
5162
|
+
: undefined;
|
|
5048
5163
|
const statusCode = isHttpError(error)
|
|
5049
5164
|
? error.statusCode
|
|
5050
5165
|
: isManagerError(error)
|
|
@@ -5052,7 +5167,6 @@ export async function buildServer(options = {}) {
|
|
|
5052
5167
|
: error instanceof ZodError
|
|
5053
5168
|
? 400
|
|
5054
5169
|
: 500;
|
|
5055
|
-
const routeUrl = request.routeOptions.url || request.url;
|
|
5056
5170
|
if (!shouldSkipAutomaticDiagnosticRoute(routeUrl)) {
|
|
5057
5171
|
try {
|
|
5058
5172
|
recordDiagnosticLog({
|
|
@@ -5093,10 +5207,11 @@ export async function buildServer(options = {}) {
|
|
|
5093
5207
|
? "invalid_request"
|
|
5094
5208
|
: "internal_error",
|
|
5095
5209
|
error: validationIssues
|
|
5096
|
-
?
|
|
5210
|
+
? `Request validation failed for ${request.method.toUpperCase()} ${routeUrl}. ${validationHelp?.validationSummary ?? ""}`.trim()
|
|
5097
5211
|
: getErrorMessage(error),
|
|
5098
5212
|
statusCode,
|
|
5099
5213
|
...(validationIssues ? { details: validationIssues } : {}),
|
|
5214
|
+
...(validationHelp ?? {}),
|
|
5100
5215
|
...(isHttpError(error) && error.details ? error.details : {}),
|
|
5101
5216
|
...(isManagerError(error) && error.details ? error.details : {})
|
|
5102
5217
|
});
|
|
@@ -5514,14 +5629,15 @@ export async function buildServer(options = {}) {
|
|
|
5514
5629
|
});
|
|
5515
5630
|
app.get("/api/v1/movement/timeline", async (request) => {
|
|
5516
5631
|
const parsed = movementTimelineQuerySchema.parse(request.query ?? {});
|
|
5632
|
+
const userIds = parsed.userIds.length > 0
|
|
5633
|
+
? parsed.userIds
|
|
5634
|
+
: (resolveScopedUserIds(request.query) ?? []);
|
|
5635
|
+
const movement = getMovementTimeline({
|
|
5636
|
+
...parsed,
|
|
5637
|
+
userIds
|
|
5638
|
+
});
|
|
5517
5639
|
return {
|
|
5518
|
-
movement:
|
|
5519
|
-
...parsed,
|
|
5520
|
-
userIds: parsed.userIds.length > 0
|
|
5521
|
-
? parsed.userIds
|
|
5522
|
-
: (resolveScopedUserIds(request.query) ??
|
|
5523
|
-
[])
|
|
5524
|
-
})
|
|
5640
|
+
movement: attachMovementTimelineSleepOverlays(movement, userIds)
|
|
5525
5641
|
};
|
|
5526
5642
|
});
|
|
5527
5643
|
app.get("/api/v1/movement/settings", async (request) => ({
|
|
@@ -5790,12 +5906,13 @@ export async function buildServer(options = {}) {
|
|
|
5790
5906
|
app.post("/api/v1/mobile/movement/timeline", async (request) => {
|
|
5791
5907
|
const parsed = movementMobileTimelineSchema.parse(request.body ?? {});
|
|
5792
5908
|
const pairing = requireValidPairing(parsed.sessionId, parsed.pairingToken);
|
|
5909
|
+
const movement = getMovementTimeline({
|
|
5910
|
+
before: parsed.before,
|
|
5911
|
+
limit: parsed.limit,
|
|
5912
|
+
userIds: [pairing.user_id]
|
|
5913
|
+
});
|
|
5793
5914
|
return {
|
|
5794
|
-
movement:
|
|
5795
|
-
before: parsed.before,
|
|
5796
|
-
limit: parsed.limit,
|
|
5797
|
-
userIds: [pairing.user_id]
|
|
5798
|
-
})
|
|
5915
|
+
movement: attachMovementTimelineSleepOverlays(movement, [pairing.user_id])
|
|
5799
5916
|
};
|
|
5800
5917
|
});
|
|
5801
5918
|
app.post("/api/v1/mobile/movement/boxes/:id/detail", async (request, reply) => {
|
|
@@ -838,6 +838,55 @@ function listSleepRows(userIds) {
|
|
|
838
838
|
ORDER BY started_at DESC`)
|
|
839
839
|
.all(...params);
|
|
840
840
|
}
|
|
841
|
+
export function getSleepTimelineOverlaysForRange(input) {
|
|
842
|
+
const rangeStartMs = Date.parse(input.startedAt);
|
|
843
|
+
const rangeEndMs = Date.parse(input.endedAt);
|
|
844
|
+
if (Number.isNaN(rangeStartMs) ||
|
|
845
|
+
Number.isNaN(rangeEndMs) ||
|
|
846
|
+
rangeEndMs <= rangeStartMs) {
|
|
847
|
+
return [];
|
|
848
|
+
}
|
|
849
|
+
return listSleepRows(input.userIds)
|
|
850
|
+
.map((row) => mapSleepSession(row))
|
|
851
|
+
.filter((session) => {
|
|
852
|
+
const sessionStartMs = Date.parse(session.startedAt);
|
|
853
|
+
const sessionEndMs = Date.parse(session.endedAt);
|
|
854
|
+
if (Number.isNaN(sessionStartMs) || Number.isNaN(sessionEndMs)) {
|
|
855
|
+
return false;
|
|
856
|
+
}
|
|
857
|
+
return sessionStartMs < rangeEndMs && sessionEndMs > rangeStartMs;
|
|
858
|
+
})
|
|
859
|
+
.sort((left, right) => {
|
|
860
|
+
const startedDelta = Date.parse(left.startedAt) - Date.parse(right.startedAt);
|
|
861
|
+
if (startedDelta !== 0) {
|
|
862
|
+
return startedDelta;
|
|
863
|
+
}
|
|
864
|
+
const endedDelta = Date.parse(left.endedAt) - Date.parse(right.endedAt);
|
|
865
|
+
if (endedDelta !== 0) {
|
|
866
|
+
return endedDelta;
|
|
867
|
+
}
|
|
868
|
+
return left.id.localeCompare(right.id);
|
|
869
|
+
})
|
|
870
|
+
.map((session) => {
|
|
871
|
+
const derived = session.derived;
|
|
872
|
+
return {
|
|
873
|
+
id: session.id,
|
|
874
|
+
externalUid: session.externalUid,
|
|
875
|
+
startedAt: session.startedAt,
|
|
876
|
+
endedAt: session.endedAt,
|
|
877
|
+
localDateKey: session.localDateKey,
|
|
878
|
+
sourceTimezone: session.sourceTimezone,
|
|
879
|
+
asleepSeconds: session.asleepSeconds,
|
|
880
|
+
timeInBedSeconds: session.timeInBedSeconds,
|
|
881
|
+
sleepScore: session.sleepScore,
|
|
882
|
+
regularityScore: session.regularityScore,
|
|
883
|
+
efficiency: typeof derived?.efficiency === "number" ? derived.efficiency : null,
|
|
884
|
+
recoveryState: typeof derived?.recoveryState === "string"
|
|
885
|
+
? derived.recoveryState
|
|
886
|
+
: null
|
|
887
|
+
};
|
|
888
|
+
});
|
|
889
|
+
}
|
|
841
890
|
function listWorkoutRows(userIds) {
|
|
842
891
|
const params = [];
|
|
843
892
|
const where = userIds && userIds.length > 0
|
|
@@ -2780,10 +2780,9 @@ function updateMovementBoxOverrideState(id, input) {
|
|
|
2780
2780
|
overridden_user_box_ids_json = ?,
|
|
2781
2781
|
override_ranges_json = ?,
|
|
2782
2782
|
is_overridden = ?,
|
|
2783
|
-
is_fully_hidden =
|
|
2784
|
-
updated_at = ?
|
|
2783
|
+
is_fully_hidden = ?
|
|
2785
2784
|
WHERE id = ?`)
|
|
2786
|
-
.run(input.overrideCount, JSON.stringify(input.overriddenAutomaticBoxIds), input.trueStartedAt, input.trueEndedAt, input.overriddenStartedAt, input.overriddenEndedAt, input.overriddenByBoxId, JSON.stringify(input.overriddenUserBoxIds), JSON.stringify(input.overrideRanges), input.isOverridden ? 1 : 0, input.isFullyHidden ? 1 : 0,
|
|
2785
|
+
.run(input.overrideCount, JSON.stringify(input.overriddenAutomaticBoxIds), input.trueStartedAt, input.trueEndedAt, input.overriddenStartedAt, input.overriddenEndedAt, input.overriddenByBoxId, JSON.stringify(input.overriddenUserBoxIds), JSON.stringify(input.overrideRanges), input.isOverridden ? 1 : 0, input.isFullyHidden ? 1 : 0, id);
|
|
2787
2786
|
}
|
|
2788
2787
|
function recomputeMovementBoxOverrideState(userId) {
|
|
2789
2788
|
const rows = listMovementBoxRows({ userIds: [userId] });
|
|
@@ -333,6 +333,18 @@ export function buildOpenApiDocument() {
|
|
|
333
333
|
message: { type: "string" }
|
|
334
334
|
}
|
|
335
335
|
};
|
|
336
|
+
const validationExpectedShape = {
|
|
337
|
+
type: "object",
|
|
338
|
+
additionalProperties: true,
|
|
339
|
+
required: ["inputShape", "requiredFields", "notes"],
|
|
340
|
+
properties: {
|
|
341
|
+
toolName: { type: "string" },
|
|
342
|
+
inputShape: { type: "string" },
|
|
343
|
+
requiredFields: arrayOf({ type: "string" }),
|
|
344
|
+
example: { type: ["string", "null"] },
|
|
345
|
+
notes: arrayOf({ type: "string" })
|
|
346
|
+
}
|
|
347
|
+
};
|
|
336
348
|
const errorResponse = {
|
|
337
349
|
type: "object",
|
|
338
350
|
additionalProperties: true,
|
|
@@ -341,7 +353,12 @@ export function buildOpenApiDocument() {
|
|
|
341
353
|
code: { type: "string" },
|
|
342
354
|
error: { type: "string" },
|
|
343
355
|
statusCode: { type: "integer" },
|
|
344
|
-
|
|
356
|
+
route: { type: "string" },
|
|
357
|
+
validationSummary: { type: "string" },
|
|
358
|
+
details: arrayOf({ $ref: "#/components/schemas/ValidationIssue" }),
|
|
359
|
+
expectedShape: {
|
|
360
|
+
$ref: "#/components/schemas/ValidationExpectedShape"
|
|
361
|
+
}
|
|
345
362
|
}
|
|
346
363
|
};
|
|
347
364
|
const tag = {
|
|
@@ -4098,6 +4115,7 @@ export function buildOpenApiDocument() {
|
|
|
4098
4115
|
components: {
|
|
4099
4116
|
schemas: {
|
|
4100
4117
|
ValidationIssue: validationIssue,
|
|
4118
|
+
ValidationExpectedShape: validationExpectedShape,
|
|
4101
4119
|
ErrorResponse: errorResponse,
|
|
4102
4120
|
Tag: tag,
|
|
4103
4121
|
Goal: goal,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useId, useRef, useState } from "react";
|
|
3
3
|
import { CircleHelp } from "lucide-react";
|
|
4
|
-
import { cn } from "
|
|
4
|
+
import { cn } from "../../lib/utils.js";
|
|
5
5
|
export function FieldHint({ children, className }) {
|
|
6
6
|
return _jsx("div", { className: cn("text-sm leading-6 text-white/50", className), children: children });
|
|
7
7
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { getEntityVisual, isEntityKind } from "
|
|
2
|
-
import { KNOWLEDGE_GRAPH_HIERARCHY_LANES, KNOWLEDGE_GRAPH_HIERARCHY_ORDER, KNOWLEDGE_GRAPH_RELATION_FAMILY_LABELS, KNOWLEDGE_GRAPH_RELATION_LABELS, buildKnowledgeGraphNodeId } from "
|
|
1
|
+
import { getEntityVisual, isEntityKind } from "./entity-visuals.js";
|
|
2
|
+
import { KNOWLEDGE_GRAPH_HIERARCHY_LANES, KNOWLEDGE_GRAPH_HIERARCHY_ORDER, KNOWLEDGE_GRAPH_RELATION_FAMILY_LABELS, KNOWLEDGE_GRAPH_RELATION_LABELS, buildKnowledgeGraphNodeId } from "./knowledge-graph-types.js";
|
|
3
3
|
export function getKnowledgeGraphNodeVisual(node) {
|
|
4
4
|
const kind = isEntityKind(node.entityKind) ? node.entityKind : "note";
|
|
5
5
|
return getEntityVisual(kind);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { forgeCustomThemeSchema, forgeThemePreferenceSchema } from "
|
|
2
|
+
import { forgeCustomThemeSchema, forgeThemePreferenceSchema } from "./theme-system.js";
|
|
3
3
|
export const appLocaleSchema = z.enum(["en", "fr"]);
|
|
4
4
|
export const inlineCreateNoteSchema = z.object({
|
|
5
5
|
contentMarkdown: z.string().trim().min(1, "Note content is required"),
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forge-openclaw-plugin",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.44",
|
|
4
4
|
"description": "Curated OpenClaw adapter for the Forge collaboration API, UI entrypoint, and localhost auto-start runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -44,17 +44,58 @@
|
|
|
44
44
|
"openclaw": "2026.4.15"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
+
"@dnd-kit/core": "^6.3.1",
|
|
48
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
49
|
+
"@dnd-kit/utilities": "^3.2.2",
|
|
47
50
|
"@azure/msal-node": "^5.1.2",
|
|
48
51
|
"@fastify/cors": "^10.0.1",
|
|
49
52
|
"@fastify/multipart": "^9.4.0",
|
|
53
|
+
"@fontsource-variable/plus-jakarta-sans": "^5.2.8",
|
|
54
|
+
"@fontsource-variable/sora": "^5.2.8",
|
|
55
|
+
"@fontsource/space-grotesk": "^5.2.10",
|
|
56
|
+
"@hookform/resolvers": "^5.1.1",
|
|
57
|
+
"@mariozechner/pi-ai": "^0.66.1",
|
|
58
|
+
"@radix-ui/react-dialog": "^1.1.14",
|
|
59
|
+
"@reduxjs/toolkit": "^2.11.2",
|
|
50
60
|
"@sinclair/typebox": "^0.34.48",
|
|
61
|
+
"@tanstack/react-query": "^5.80.10",
|
|
62
|
+
"@tanstack/react-table": "^8.21.3",
|
|
63
|
+
"@tanstack/react-virtual": "^3.13.12",
|
|
64
|
+
"@tauri-apps/api": "^2.8.0",
|
|
65
|
+
"@tiptap/react": "^2.15.0",
|
|
66
|
+
"@tiptap/starter-kit": "^2.15.0",
|
|
67
|
+
"@types/react-grid-layout": "^1.3.6",
|
|
68
|
+
"@xyflow/react": "^12.10.2",
|
|
51
69
|
"adm-zip": "^0.5.17",
|
|
70
|
+
"bonjour-service": "^1.3.0",
|
|
71
|
+
"class-variance-authority": "^0.7.1",
|
|
72
|
+
"clsx": "^2.1.1",
|
|
73
|
+
"cron-parser": "^5.5.0",
|
|
52
74
|
"fastify": "^5.8.5",
|
|
75
|
+
"framer-motion": "^12.16.0",
|
|
76
|
+
"graphology": "^0.26.0",
|
|
77
|
+
"graphology-layout": "^0.6.1",
|
|
78
|
+
"graphology-layout-forceatlas2": "^0.10.1",
|
|
79
|
+
"lucide-react": "^0.525.0",
|
|
53
80
|
"node-ical": "^0.20.1",
|
|
81
|
+
"qrcode": "^1.5.4",
|
|
82
|
+
"react": "^19.1.0",
|
|
83
|
+
"react-arborist": "^3.4.3",
|
|
84
|
+
"react-dom": "^19.1.0",
|
|
85
|
+
"react-grid-layout": "^2.2.3",
|
|
86
|
+
"react-hook-form": "^7.57.0",
|
|
87
|
+
"react-is": "^19.2.5",
|
|
88
|
+
"react-redux": "^9.2.0",
|
|
89
|
+
"react-router-dom": "^7.13.1",
|
|
90
|
+
"recharts": "^3.1.0",
|
|
91
|
+
"sigma": "^3.0.2",
|
|
92
|
+
"tailwind-merge": "^3.3.1",
|
|
54
93
|
"tsdav": "^2.1.8",
|
|
55
|
-
"zod": "^3.25.67"
|
|
94
|
+
"zod": "^3.25.67",
|
|
95
|
+
"zustand": "^5.0.5"
|
|
56
96
|
},
|
|
57
97
|
"overrides": {
|
|
98
|
+
"basic-ftp": "^5.3.0",
|
|
58
99
|
"axios": "^1.15.0",
|
|
59
100
|
"follow-redirects": "^1.16.0",
|
|
60
101
|
"hono": "4.12.14"
|