forge-openclaw-plugin 0.2.24 → 0.2.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/dist/assets/{board-_C6oMy5w.js → board-ta0rUHOf.js} +3 -3
- package/dist/assets/{board-_C6oMy5w.js.map → board-ta0rUHOf.js.map} +1 -1
- package/dist/assets/index-Ro0ZF_az.css +1 -0
- package/dist/assets/index-ytlpSj23.js +79 -0
- package/dist/assets/index-ytlpSj23.js.map +1 -0
- package/dist/assets/{motion-D4sZgCHd.js → motion-fBKPB6yw.js} +3 -3
- package/dist/assets/motion-fBKPB6yw.js.map +1 -0
- package/dist/assets/{table-BWzTaky1.js → table-C-IGTQni.js} +2 -2
- package/dist/assets/{table-BWzTaky1.js.map → table-C-IGTQni.js.map} +1 -1
- package/dist/assets/{ui-BzK4azQb.js → ui-DInOpaYF.js} +2 -2
- package/dist/assets/{ui-BzK4azQb.js.map → ui-DInOpaYF.js.map} +1 -1
- package/dist/assets/vendor-lE3tZJcC.js +876 -0
- package/dist/assets/vendor-lE3tZJcC.js.map +1 -0
- package/dist/index.html +7 -8
- package/dist/openclaw/local-runtime.d.ts +3 -1
- package/dist/openclaw/local-runtime.js +51 -15
- package/dist/openclaw/parity.d.ts +1 -1
- package/dist/openclaw/parity.js +29 -0
- package/dist/openclaw/plugin-entry-shared.d.ts +1 -0
- package/dist/openclaw/plugin-entry-shared.js +31 -6
- package/dist/openclaw/plugin-sdk-types.d.ts +29 -0
- package/dist/openclaw/routes.js +236 -0
- package/dist/openclaw/session-bootstrap.d.ts +78 -0
- package/dist/openclaw/session-bootstrap.js +240 -0
- package/dist/openclaw/tools.js +279 -6
- package/dist/server/server/migrations/001_core.sql +411 -0
- package/dist/server/server/migrations/002_psyche.sql +392 -0
- package/dist/server/server/migrations/003_habits.sql +30 -0
- package/dist/server/server/migrations/004_habit_links.sql +8 -0
- package/dist/server/server/migrations/005_habit_psyche_links.sql +24 -0
- package/dist/server/server/migrations/006_work_adjustments.sql +14 -0
- package/dist/server/server/migrations/007_weekly_review_closures.sql +17 -0
- package/dist/server/server/migrations/008_calendar_execution.sql +147 -0
- package/dist/server/server/migrations/009_true_calendar_events.sql +195 -0
- package/dist/server/server/migrations/010_calendar_selection_state.sql +6 -0
- package/dist/server/server/migrations/011_calendar_timezone_backfill.sql +11 -0
- package/dist/server/server/migrations/012_work_block_ranges.sql +7 -0
- package/dist/server/server/migrations/013_microsoft_local_auth_settings.sql +8 -0
- package/dist/server/server/migrations/014_note_tags_and_ephemeral.sql +8 -0
- package/dist/server/server/migrations/015_multi_user_and_strategies.sql +244 -0
- package/dist/server/server/migrations/016_health_companion.sql +158 -0
- package/dist/server/server/migrations/016_strategy_contracts_and_user_graph.sql +22 -0
- package/dist/server/server/migrations/017_preferences.sql +131 -0
- package/dist/server/server/migrations/018_preference_catalogs.sql +31 -0
- package/dist/server/server/migrations/019_wiki_memory.sql +255 -0
- package/dist/server/server/migrations/020_wiki_page_hierarchy.sql +11 -0
- package/dist/server/server/migrations/021_hide_evidence_from_wiki_index.sql +3 -0
- package/dist/server/server/migrations/022_wiki_ingest_background.sql +85 -0
- package/dist/server/server/migrations/023_diagnostic_logs.sql +28 -0
- package/dist/server/server/migrations/024_questionnaires.sql +96 -0
- package/dist/server/server/migrations/025_ai_model_connections.sql +26 -0
- package/dist/server/server/migrations/026_custom_theme_settings.sql +2 -0
- package/dist/server/server/migrations/027_ai_processors.sql +31 -0
- package/dist/server/server/migrations/028_movement_domain.sql +136 -0
- package/dist/server/server/migrations/029_watch_micro_capture.sql +23 -0
- package/dist/server/server/migrations/030_surface_layouts.sql +5 -0
- package/dist/server/server/migrations/031_ai_processor_runtime_upgrades.sql +10 -0
- package/dist/server/server/migrations/032_ai_connectors.sql +44 -0
- package/dist/server/server/migrations/033_movement_trip_point_sync.sql +36 -0
- package/dist/server/server/migrations/034_movement_segment_sync.sql +49 -0
- package/dist/server/server/migrations/035_google_local_auth_settings.sql +2 -0
- package/dist/server/server/migrations/036_google_local_auth_client_secret.sql +2 -0
- package/dist/server/{app.js → server/src/app.js} +992 -25
- package/dist/server/server/src/connectors/box-registry.js +188 -0
- package/dist/server/{db.js → server/src/db.js} +6 -0
- package/dist/server/server/src/debug.js +19 -0
- package/dist/server/server/src/discovery-advertiser.js +114 -0
- package/dist/server/{health.js → server/src/health.js} +39 -11
- package/dist/server/{index.js → server/src/index.js} +4 -0
- package/dist/server/{managers → server/src/managers}/platform/llm-manager.js +40 -4
- package/dist/server/{managers → server/src/managers}/platform/openai-responses-provider.js +129 -19
- package/dist/server/server/src/movement.js +2935 -0
- package/dist/server/{openapi.js → server/src/openapi.js} +628 -5
- package/dist/server/{psyche-types.js → server/src/psyche-types.js} +15 -1
- package/dist/server/server/src/questionnaire-flow.js +552 -0
- package/dist/server/server/src/questionnaire-seeds.js +853 -0
- package/dist/server/server/src/questionnaire-types.js +340 -0
- package/dist/server/server/src/repositories/ai-connectors.js +1207 -0
- package/dist/server/server/src/repositories/ai-processors.js +547 -0
- package/dist/server/{repositories → server/src/repositories}/calendar.js +1 -1
- package/dist/server/{repositories → server/src/repositories}/entity-ownership.js +9 -1
- package/dist/server/{repositories → server/src/repositories}/habits.js +69 -5
- package/dist/server/server/src/repositories/model-settings.js +216 -0
- package/dist/server/{repositories → server/src/repositories}/notes.js +57 -15
- package/dist/server/{repositories → server/src/repositories}/preferences.js +124 -0
- package/dist/server/server/src/repositories/questionnaires.js +1338 -0
- package/dist/server/{repositories → server/src/repositories}/settings.js +156 -12
- package/dist/server/server/src/repositories/surface-layouts.js +76 -0
- package/dist/server/{repositories → server/src/repositories}/wiki-memory.js +5 -1
- package/dist/server/{services → server/src/services}/calendar-runtime.js +775 -58
- package/dist/server/{services → server/src/services}/entity-crud.js +81 -2
- package/dist/server/server/src/services/google-calendar-oauth-config.js +176 -0
- package/dist/server/server/src/services/openai-codex-oauth.js +153 -0
- package/dist/server/server/src/services/psyche-observation-calendar.js +46 -0
- package/dist/server/{types.js → server/src/types.js} +621 -14
- package/dist/server/server/src/watch-mobile.js +562 -0
- package/dist/server/{web.js → server/src/web.js} +30 -4
- package/dist/server/src/components/customization/utility-widgets.js +330 -0
- package/dist/server/src/components/workbench-boxes/health/health-boxes.js +92 -0
- package/dist/server/src/components/workbench-boxes/kanban/kanban-boxes.js +128 -0
- package/dist/server/src/components/workbench-boxes/movement/movement-boxes.js +37 -0
- package/dist/server/src/components/workbench-boxes/notes/notes-boxes.js +114 -0
- package/dist/server/src/components/workbench-boxes/projects/projects-boxes.js +57 -0
- package/dist/server/src/components/workbench-boxes/shared/define-workbench-box.js +4 -0
- package/dist/server/src/components/workbench-boxes/shared/generic-node-view.js +13 -0
- package/dist/server/src/components/workbench-boxes/today/today-boxes.js +63 -0
- package/dist/server/src/lib/api-error.js +37 -0
- package/dist/server/src/lib/api.js +1859 -0
- package/dist/server/src/lib/calendar-name-deduper.js +144 -0
- package/dist/server/src/lib/diagnostics.js +67 -0
- package/dist/server/src/lib/psyche-types.js +1 -0
- package/dist/server/src/lib/questionnaire-types.js +1 -0
- package/dist/server/src/lib/runtime-paths.js +24 -0
- package/dist/server/src/lib/schemas.js +234 -0
- package/dist/server/src/lib/snapshot-normalizer.js +374 -0
- package/dist/server/src/lib/theme-system.js +319 -0
- package/dist/server/src/lib/types.js +1 -0
- package/dist/server/src/lib/utils.js +22 -0
- package/dist/server/src/lib/workbench/boxes.js +16 -0
- package/dist/server/src/lib/workbench/nodes.js +15 -0
- package/dist/server/src/lib/workbench/registry.js +73 -0
- package/dist/server/src/lib/workbench/runtime.js +181 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +6 -1
- package/server/index.js +68 -0
- package/server/migrations/024_questionnaires.sql +96 -0
- package/server/migrations/025_ai_model_connections.sql +26 -0
- package/server/migrations/026_custom_theme_settings.sql +2 -0
- package/server/migrations/027_ai_processors.sql +31 -0
- package/server/migrations/028_movement_domain.sql +136 -0
- package/server/migrations/029_watch_micro_capture.sql +23 -0
- package/server/migrations/030_surface_layouts.sql +5 -0
- package/server/migrations/031_ai_processor_runtime_upgrades.sql +10 -0
- package/server/migrations/032_ai_connectors.sql +44 -0
- package/server/migrations/033_movement_trip_point_sync.sql +36 -0
- package/server/migrations/034_movement_segment_sync.sql +49 -0
- package/server/migrations/035_google_local_auth_settings.sql +2 -0
- package/server/migrations/036_google_local_auth_client_secret.sql +2 -0
- package/skills/forge-openclaw/SKILL.md +15 -1
- package/skills/forge-openclaw/entity_conversation_playbooks.md +523 -87
- package/skills/forge-openclaw/psyche_entity_playbooks.md +331 -221
- package/dist/assets/index-DTCwBWAs.js +0 -65
- package/dist/assets/index-DTCwBWAs.js.map +0 -1
- package/dist/assets/index-DttXlAgi.css +0 -1
- package/dist/assets/motion-D4sZgCHd.js.map +0 -1
- package/dist/assets/vendor-De38P6YR.js +0 -729
- package/dist/assets/vendor-De38P6YR.js.map +0 -1
- package/dist/assets/viz-C6hfyqzu.js +0 -34
- package/dist/assets/viz-C6hfyqzu.js.map +0 -1
- package/skills/forge-openclaw/cron_jobs.md +0 -395
- /package/dist/server/{demo-data.js → server/src/demo-data.js} +0 -0
- /package/dist/server/{e2e-server.js → server/src/e2e-server.js} +0 -0
- /package/dist/server/{errors.js → server/src/errors.js} +0 -0
- /package/dist/server/{managers → server/src/managers}/base.js +0 -0
- /package/dist/server/{managers → server/src/managers}/contracts.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/api-gateway-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/audit-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/authentication-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/authorization-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/background-job-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/configuration-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/database-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/event-bus-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/external-service-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/health-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/migration-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/search-index-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/secrets-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/session-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/storage-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/token-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/transaction-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/trusted-network.js +0 -0
- /package/dist/server/{managers → server/src/managers}/runtime.js +0 -0
- /package/dist/server/{managers → server/src/managers}/type-guards.js +0 -0
- /package/dist/server/{preferences-seeds.js → server/src/preferences-seeds.js} +0 -0
- /package/dist/server/{preferences-types.js → server/src/preferences-types.js} +0 -0
- /package/dist/server/{repositories → server/src/repositories}/activity-events.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/collaboration.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/deleted-entities.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/diagnostic-logs.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/domains.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/event-log.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/goals.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/projects.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/psyche.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/rewards.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/strategies.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/tags.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/task-runs.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/tasks.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/users.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/weekly-reviews.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/work-adjustments.js +0 -0
- /package/dist/server/{seed-demo.js → server/src/seed-demo.js} +0 -0
- /package/dist/server/{services → server/src/services}/context.js +0 -0
- /package/dist/server/{services → server/src/services}/dashboard.js +0 -0
- /package/dist/server/{services → server/src/services}/gamification.js +0 -0
- /package/dist/server/{services → server/src/services}/insights.js +0 -0
- /package/dist/server/{services → server/src/services}/projects.js +0 -0
- /package/dist/server/{services → server/src/services}/psyche.js +0 -0
- /package/dist/server/{services → server/src/services}/relations.js +0 -0
- /package/dist/server/{services → server/src/services}/reviews.js +0 -0
- /package/dist/server/{services → server/src/services}/run-recovery.js +0 -0
- /package/dist/server/{services → server/src/services}/tagging.js +0 -0
- /package/dist/server/{services → server/src/services}/task-run-watchdog.js +0 -0
- /package/dist/server/{services → server/src/services}/work-time.js +0 -0
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { execFile as execFileCallback } from "node:child_process";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { getDatabase } from "../db.js";
|
|
7
|
+
import { aiProcessorLinkSchema, aiProcessorSchema, createAiProcessorLinkSchema, createAiProcessorSchema, runAiProcessorSchema, surfaceProcessorGraphPayloadSchema, updateAiProcessorSchema } from "../types.js";
|
|
8
|
+
import { FORGE_DEFAULT_AGENT_ID, getAiModelConnectionById, listAiModelConnections, readModelConnectionCredential } from "./model-settings.js";
|
|
9
|
+
import { getSettings } from "./settings.js";
|
|
10
|
+
const MAX_RUN_HISTORY = 12;
|
|
11
|
+
const MAX_TOOL_STEPS = 6;
|
|
12
|
+
const execFile = promisify(execFileCallback);
|
|
13
|
+
function parseJson(value, fallback) {
|
|
14
|
+
try {
|
|
15
|
+
return value ? JSON.parse(value) : fallback;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return fallback;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function slugifySegment(value) {
|
|
22
|
+
const normalized = value
|
|
23
|
+
.trim()
|
|
24
|
+
.toLowerCase()
|
|
25
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
26
|
+
.replace(/^-+|-+$/g, "");
|
|
27
|
+
return normalized || "processor";
|
|
28
|
+
}
|
|
29
|
+
function buildProcessorSlug(title, id) {
|
|
30
|
+
return `${slugifySegment(title)}-${id.slice(-6)}`;
|
|
31
|
+
}
|
|
32
|
+
function processorWidgetId(processorId) {
|
|
33
|
+
return `aiproc:${processorId}`;
|
|
34
|
+
}
|
|
35
|
+
function processorIdFromNodeId(nodeId) {
|
|
36
|
+
return nodeId.startsWith("aiproc:") ? nodeId.slice("aiproc:".length) : null;
|
|
37
|
+
}
|
|
38
|
+
function resolveAllowedPath(inputPath) {
|
|
39
|
+
const candidate = path.resolve(process.cwd(), inputPath);
|
|
40
|
+
const workspaceRoot = process.cwd();
|
|
41
|
+
if (candidate !== workspaceRoot &&
|
|
42
|
+
!candidate.startsWith(`${workspaceRoot}${path.sep}`)) {
|
|
43
|
+
throw new Error("Machine access is restricted to the Forge workspace root.");
|
|
44
|
+
}
|
|
45
|
+
return candidate;
|
|
46
|
+
}
|
|
47
|
+
function tryParseStructuredAgentResponse(value) {
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(value);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function executeMachineTool(processor, tool, args) {
|
|
56
|
+
if (tool === "machine_read_file") {
|
|
57
|
+
if (!processor.machineAccess.read) {
|
|
58
|
+
throw new Error("Read access is disabled for this processor.");
|
|
59
|
+
}
|
|
60
|
+
const targetPath = typeof args.path === "string" ? resolveAllowedPath(args.path) : null;
|
|
61
|
+
if (!targetPath) {
|
|
62
|
+
throw new Error("machine_read_file requires a string path.");
|
|
63
|
+
}
|
|
64
|
+
const content = await readFile(targetPath, "utf8");
|
|
65
|
+
return {
|
|
66
|
+
path: targetPath,
|
|
67
|
+
content
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (tool === "machine_write_file") {
|
|
71
|
+
if (!processor.machineAccess.write) {
|
|
72
|
+
throw new Error("Write access is disabled for this processor.");
|
|
73
|
+
}
|
|
74
|
+
const targetPath = typeof args.path === "string" ? resolveAllowedPath(args.path) : null;
|
|
75
|
+
if (!targetPath) {
|
|
76
|
+
throw new Error("machine_write_file requires a string path.");
|
|
77
|
+
}
|
|
78
|
+
if (typeof args.content !== "string") {
|
|
79
|
+
throw new Error("machine_write_file requires string content.");
|
|
80
|
+
}
|
|
81
|
+
await writeFile(targetPath, args.content, "utf8");
|
|
82
|
+
return {
|
|
83
|
+
path: targetPath,
|
|
84
|
+
bytesWritten: Buffer.byteLength(args.content, "utf8")
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (!processor.machineAccess.exec) {
|
|
88
|
+
throw new Error("Exec access is disabled for this processor.");
|
|
89
|
+
}
|
|
90
|
+
if (typeof args.command !== "string" || args.command.trim().length === 0) {
|
|
91
|
+
throw new Error("machine_exec requires a command string.");
|
|
92
|
+
}
|
|
93
|
+
const cwd = typeof args.cwd === "string" && args.cwd.trim().length > 0
|
|
94
|
+
? resolveAllowedPath(args.cwd)
|
|
95
|
+
: process.cwd();
|
|
96
|
+
const result = await execFile("zsh", ["-lc", args.command], {
|
|
97
|
+
cwd,
|
|
98
|
+
timeout: 15_000,
|
|
99
|
+
maxBuffer: 256_000
|
|
100
|
+
});
|
|
101
|
+
return {
|
|
102
|
+
cwd,
|
|
103
|
+
stdout: result.stdout.trim(),
|
|
104
|
+
stderr: result.stderr.trim()
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
async function runProcessorAgent(processor, agent, fullPrompt, services) {
|
|
108
|
+
if (!agent.profile) {
|
|
109
|
+
return "No model connection is configured for this agent yet.";
|
|
110
|
+
}
|
|
111
|
+
const toolNames = [
|
|
112
|
+
processor.machineAccess.read ? "machine_read_file(path)" : null,
|
|
113
|
+
processor.machineAccess.write
|
|
114
|
+
? "machine_write_file(path, content)"
|
|
115
|
+
: null,
|
|
116
|
+
processor.machineAccess.exec ? "machine_exec(command, cwd?)" : null
|
|
117
|
+
].filter(Boolean);
|
|
118
|
+
if (toolNames.length === 0) {
|
|
119
|
+
const result = await services.llm.runTextPrompt(agent.profile, {
|
|
120
|
+
explicitApiKey: agent.explicitApiKey,
|
|
121
|
+
systemPrompt: "You are an AI processor inside Forge. Follow the prompt flow exactly, use the linked context carefully, and return only the final output for your assigned agent.",
|
|
122
|
+
prompt: fullPrompt
|
|
123
|
+
});
|
|
124
|
+
return result.outputText.trim();
|
|
125
|
+
}
|
|
126
|
+
const transcript = [];
|
|
127
|
+
for (let step = 0; step < MAX_TOOL_STEPS; step += 1) {
|
|
128
|
+
const result = await services.llm.runTextPrompt(agent.profile, {
|
|
129
|
+
explicitApiKey: agent.explicitApiKey,
|
|
130
|
+
systemPrompt: [
|
|
131
|
+
"You are an AI processor inside Forge.",
|
|
132
|
+
"You may use machine tools when they are enabled.",
|
|
133
|
+
`Available tools: ${toolNames.join(", ")}.`,
|
|
134
|
+
"Return strict JSON only.",
|
|
135
|
+
'For a final answer, return {"action":"final","text":"..."}',
|
|
136
|
+
'To call a tool, return {"action":"tool","tool":"machine_exec","args":{...}}'
|
|
137
|
+
].join(" "),
|
|
138
|
+
prompt: [
|
|
139
|
+
fullPrompt,
|
|
140
|
+
transcript.length > 0
|
|
141
|
+
? `Tool transcript:\n${transcript.join("\n\n")}`
|
|
142
|
+
: ""
|
|
143
|
+
]
|
|
144
|
+
.filter(Boolean)
|
|
145
|
+
.join("\n\n")
|
|
146
|
+
});
|
|
147
|
+
const structured = tryParseStructuredAgentResponse(result.outputText.trim());
|
|
148
|
+
if (!structured || structured.action === "final") {
|
|
149
|
+
return structured?.text?.trim() || result.outputText.trim();
|
|
150
|
+
}
|
|
151
|
+
const toolResult = await executeMachineTool(processor, structured.tool, structured.args);
|
|
152
|
+
transcript.push(`Tool call ${structured.tool}: ${JSON.stringify(structured.args)}`, `Tool result: ${JSON.stringify(toolResult)}`);
|
|
153
|
+
}
|
|
154
|
+
return "Processor stopped after reaching the maximum tool step count.";
|
|
155
|
+
}
|
|
156
|
+
function mapProcessor(row) {
|
|
157
|
+
return aiProcessorSchema.parse({
|
|
158
|
+
id: row.id,
|
|
159
|
+
slug: row.slug,
|
|
160
|
+
surfaceId: row.surface_id,
|
|
161
|
+
title: row.title,
|
|
162
|
+
promptFlow: row.prompt_flow,
|
|
163
|
+
contextInput: row.context_input,
|
|
164
|
+
toolConfig: parseJson(row.tool_config_json, []),
|
|
165
|
+
agentIds: parseJson(row.agent_ids_json, []),
|
|
166
|
+
agentConfigs: parseJson(row.agent_config_json, []),
|
|
167
|
+
triggerMode: row.trigger_mode,
|
|
168
|
+
cronExpression: row.cron_expression,
|
|
169
|
+
machineAccess: parseJson(row.machine_access_json, {
|
|
170
|
+
read: false,
|
|
171
|
+
write: false,
|
|
172
|
+
exec: false
|
|
173
|
+
}),
|
|
174
|
+
endpointEnabled: row.endpoint_enabled === 1,
|
|
175
|
+
lastRunAt: row.last_run_at,
|
|
176
|
+
lastRunStatus: row.last_run_status,
|
|
177
|
+
lastRunOutput: parseJson(row.last_run_output_json, null),
|
|
178
|
+
runHistory: parseJson(row.run_history_json, []),
|
|
179
|
+
createdAt: row.created_at,
|
|
180
|
+
updatedAt: row.updated_at
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
function mapLink(row) {
|
|
184
|
+
return aiProcessorLinkSchema.parse({
|
|
185
|
+
id: row.id,
|
|
186
|
+
surfaceId: row.surface_id,
|
|
187
|
+
sourceWidgetId: row.source_widget_id,
|
|
188
|
+
targetProcessorId: row.target_processor_id,
|
|
189
|
+
accessMode: row.access_mode,
|
|
190
|
+
capabilityMode: row.capability_mode,
|
|
191
|
+
metadata: parseJson(row.metadata_json, {}),
|
|
192
|
+
createdAt: row.created_at,
|
|
193
|
+
updatedAt: row.updated_at
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
export function listAiProcessors(surfaceId) {
|
|
197
|
+
const rows = surfaceId
|
|
198
|
+
? (getDatabase()
|
|
199
|
+
.prepare(`SELECT * FROM ai_processors WHERE surface_id = ? ORDER BY created_at ASC`)
|
|
200
|
+
.all(surfaceId) ?? [])
|
|
201
|
+
: (getDatabase()
|
|
202
|
+
.prepare(`SELECT * FROM ai_processors ORDER BY created_at ASC`)
|
|
203
|
+
.all() ?? []);
|
|
204
|
+
return rows.map(mapProcessor);
|
|
205
|
+
}
|
|
206
|
+
export function getAiProcessorById(processorId) {
|
|
207
|
+
const row = getDatabase()
|
|
208
|
+
.prepare(`SELECT * FROM ai_processors WHERE id = ?`)
|
|
209
|
+
.get(processorId);
|
|
210
|
+
return row ? mapProcessor(row) : null;
|
|
211
|
+
}
|
|
212
|
+
export function getAiProcessorBySlug(slug) {
|
|
213
|
+
const row = getDatabase()
|
|
214
|
+
.prepare(`SELECT * FROM ai_processors WHERE slug = ?`)
|
|
215
|
+
.get(slug);
|
|
216
|
+
return row ? mapProcessor(row) : null;
|
|
217
|
+
}
|
|
218
|
+
export function listAiProcessorLinks(surfaceId) {
|
|
219
|
+
const rows = surfaceId
|
|
220
|
+
? (getDatabase()
|
|
221
|
+
.prepare(`SELECT * FROM ai_processor_links WHERE surface_id = ? ORDER BY created_at ASC`)
|
|
222
|
+
.all(surfaceId) ?? [])
|
|
223
|
+
: (getDatabase()
|
|
224
|
+
.prepare(`SELECT * FROM ai_processor_links ORDER BY created_at ASC`)
|
|
225
|
+
.all() ?? []);
|
|
226
|
+
return rows.map(mapLink);
|
|
227
|
+
}
|
|
228
|
+
export function getSurfaceProcessorGraph(surfaceId) {
|
|
229
|
+
return surfaceProcessorGraphPayloadSchema.parse({
|
|
230
|
+
surfaceId,
|
|
231
|
+
processors: listAiProcessors(surfaceId),
|
|
232
|
+
links: listAiProcessorLinks(surfaceId)
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
export function createAiProcessor(input) {
|
|
236
|
+
const parsed = createAiProcessorSchema.parse(input);
|
|
237
|
+
const now = new Date().toISOString();
|
|
238
|
+
const id = `aip_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
|
|
239
|
+
const slug = buildProcessorSlug(parsed.title, id);
|
|
240
|
+
getDatabase()
|
|
241
|
+
.prepare(`INSERT INTO ai_processors (
|
|
242
|
+
id, slug, surface_id, title, prompt_flow, context_input, tool_config_json, agent_ids_json, agent_config_json, trigger_mode, cron_expression, machine_access_json, endpoint_enabled, last_run_at, last_run_status, last_run_output_json, run_history_json, created_at, updated_at
|
|
243
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
244
|
+
.run(id, slug, parsed.surfaceId, parsed.title, parsed.promptFlow, parsed.contextInput, JSON.stringify(parsed.toolConfig), JSON.stringify(parsed.agentIds), JSON.stringify(parsed.agentConfigs), parsed.triggerMode, parsed.cronExpression, JSON.stringify(parsed.machineAccess), parsed.endpointEnabled ? 1 : 0, null, "idle", null, "[]", now, now);
|
|
245
|
+
return getAiProcessorById(id);
|
|
246
|
+
}
|
|
247
|
+
export function updateAiProcessor(processorId, patch) {
|
|
248
|
+
const current = getAiProcessorById(processorId);
|
|
249
|
+
if (!current) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
const parsed = updateAiProcessorSchema.parse(patch);
|
|
253
|
+
const next = {
|
|
254
|
+
...current,
|
|
255
|
+
...parsed,
|
|
256
|
+
slug: parsed.title && parsed.title !== current.title
|
|
257
|
+
? buildProcessorSlug(parsed.title, current.id)
|
|
258
|
+
: current.slug,
|
|
259
|
+
machineAccess: {
|
|
260
|
+
...current.machineAccess,
|
|
261
|
+
...(parsed.machineAccess ?? {})
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
const now = new Date().toISOString();
|
|
265
|
+
getDatabase()
|
|
266
|
+
.prepare(`UPDATE ai_processors
|
|
267
|
+
SET slug = ?, title = ?, prompt_flow = ?, context_input = ?, tool_config_json = ?, agent_ids_json = ?, agent_config_json = ?, trigger_mode = ?, cron_expression = ?, machine_access_json = ?, endpoint_enabled = ?, updated_at = ?
|
|
268
|
+
WHERE id = ?`)
|
|
269
|
+
.run(next.slug, next.title, next.promptFlow, next.contextInput, JSON.stringify(next.toolConfig), JSON.stringify(next.agentIds), JSON.stringify(next.agentConfigs), next.triggerMode, next.cronExpression, JSON.stringify(next.machineAccess), next.endpointEnabled ? 1 : 0, now, processorId);
|
|
270
|
+
return getAiProcessorById(processorId);
|
|
271
|
+
}
|
|
272
|
+
export function deleteAiProcessor(processorId) {
|
|
273
|
+
const current = getAiProcessorById(processorId);
|
|
274
|
+
if (!current) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
getDatabase().prepare(`DELETE FROM ai_processors WHERE id = ?`).run(processorId);
|
|
278
|
+
return current;
|
|
279
|
+
}
|
|
280
|
+
function assertProcessorGraphEdgeIsValid(input) {
|
|
281
|
+
const sourceProcessorId = processorIdFromNodeId(input.sourceWidgetId);
|
|
282
|
+
if (!sourceProcessorId) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (sourceProcessorId === input.targetProcessorId) {
|
|
286
|
+
throw new Error("AI processor links cannot point a processor to itself.");
|
|
287
|
+
}
|
|
288
|
+
const links = listAiProcessorLinks(input.surfaceId);
|
|
289
|
+
const adjacency = new Map();
|
|
290
|
+
for (const link of links) {
|
|
291
|
+
const upstreamProcessorId = processorIdFromNodeId(link.sourceWidgetId);
|
|
292
|
+
if (!upstreamProcessorId) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
const current = adjacency.get(upstreamProcessorId) ?? new Set();
|
|
296
|
+
current.add(link.targetProcessorId);
|
|
297
|
+
adjacency.set(upstreamProcessorId, current);
|
|
298
|
+
}
|
|
299
|
+
const nextTargets = adjacency.get(sourceProcessorId) ?? new Set();
|
|
300
|
+
nextTargets.add(input.targetProcessorId);
|
|
301
|
+
adjacency.set(sourceProcessorId, nextTargets);
|
|
302
|
+
const seen = new Set();
|
|
303
|
+
const stack = [input.targetProcessorId];
|
|
304
|
+
while (stack.length > 0) {
|
|
305
|
+
const current = stack.pop();
|
|
306
|
+
if (current === sourceProcessorId) {
|
|
307
|
+
throw new Error("This link would create a processor cycle.");
|
|
308
|
+
}
|
|
309
|
+
if (seen.has(current)) {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
seen.add(current);
|
|
313
|
+
for (const next of adjacency.get(current) ?? []) {
|
|
314
|
+
stack.push(next);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
export function createAiProcessorLink(input) {
|
|
319
|
+
const parsed = createAiProcessorLinkSchema.parse(input);
|
|
320
|
+
assertProcessorGraphEdgeIsValid(parsed);
|
|
321
|
+
const existing = listAiProcessorLinks(parsed.surfaceId).find((link) => link.sourceWidgetId === parsed.sourceWidgetId &&
|
|
322
|
+
link.targetProcessorId === parsed.targetProcessorId);
|
|
323
|
+
const now = new Date().toISOString();
|
|
324
|
+
const id = existing?.id ?? `ail_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
|
|
325
|
+
getDatabase()
|
|
326
|
+
.prepare(`INSERT INTO ai_processor_links (
|
|
327
|
+
id, surface_id, source_widget_id, target_processor_id, access_mode, capability_mode, metadata_json, created_at, updated_at
|
|
328
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
329
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
330
|
+
access_mode = excluded.access_mode,
|
|
331
|
+
capability_mode = excluded.capability_mode,
|
|
332
|
+
metadata_json = excluded.metadata_json,
|
|
333
|
+
updated_at = excluded.updated_at`)
|
|
334
|
+
.run(id, parsed.surfaceId, parsed.sourceWidgetId, parsed.targetProcessorId, parsed.accessMode, parsed.capabilityMode, JSON.stringify(parsed.metadata), existing?.createdAt ?? now, now);
|
|
335
|
+
return listAiProcessorLinks(parsed.surfaceId).find((entry) => entry.id === id);
|
|
336
|
+
}
|
|
337
|
+
export function deleteAiProcessorLink(linkId) {
|
|
338
|
+
const existing = listAiProcessorLinks().find((entry) => entry.id === linkId);
|
|
339
|
+
if (!existing) {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
getDatabase().prepare(`DELETE FROM ai_processor_links WHERE id = ?`).run(linkId);
|
|
343
|
+
return existing;
|
|
344
|
+
}
|
|
345
|
+
function resolveProcessorAgentProfiles(processor, secrets) {
|
|
346
|
+
const allConnections = listAiModelConnections();
|
|
347
|
+
const requestedAgentIds = processor.agentIds.length > 0 ? processor.agentIds : [FORGE_DEFAULT_AGENT_ID];
|
|
348
|
+
const configByAgentId = new Map(processor.agentConfigs.map((config) => [config.agentId, config]));
|
|
349
|
+
const settings = getSettings();
|
|
350
|
+
return requestedAgentIds.map((agentId) => {
|
|
351
|
+
const override = configByAgentId.get(agentId) ?? null;
|
|
352
|
+
let connection = (override?.connectionId
|
|
353
|
+
? getAiModelConnectionById(override.connectionId)
|
|
354
|
+
: null) ??
|
|
355
|
+
allConnections.find((entry) => entry.agentId === agentId) ??
|
|
356
|
+
null;
|
|
357
|
+
if (agentId === FORGE_DEFAULT_AGENT_ID) {
|
|
358
|
+
const selected = settings.modelSettings.forgeAgent.basicChat.connectionId;
|
|
359
|
+
connection =
|
|
360
|
+
(override?.connectionId ? connection : null) ??
|
|
361
|
+
(selected ? getAiModelConnectionById(selected) : null);
|
|
362
|
+
}
|
|
363
|
+
if (!connection) {
|
|
364
|
+
return {
|
|
365
|
+
agentId,
|
|
366
|
+
agentLabel: agentId === FORGE_DEFAULT_AGENT_ID ? "Forge Agent" : agentId,
|
|
367
|
+
profile: null,
|
|
368
|
+
explicitApiKey: null
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
const credential = readModelConnectionCredential(connection.id, secrets);
|
|
372
|
+
const explicitApiKey = credential?.kind === "api_key"
|
|
373
|
+
? credential.apiKey
|
|
374
|
+
: credential?.kind === "oauth"
|
|
375
|
+
? credential.access
|
|
376
|
+
: null;
|
|
377
|
+
return {
|
|
378
|
+
agentId,
|
|
379
|
+
agentLabel: agentId === FORGE_DEFAULT_AGENT_ID
|
|
380
|
+
? "Forge Agent"
|
|
381
|
+
: connection.agentLabel,
|
|
382
|
+
profile: {
|
|
383
|
+
provider: connection.provider,
|
|
384
|
+
baseUrl: connection.baseUrl,
|
|
385
|
+
model: override?.model?.trim() || connection.model,
|
|
386
|
+
systemPrompt: "",
|
|
387
|
+
secretId: null,
|
|
388
|
+
metadata: {}
|
|
389
|
+
},
|
|
390
|
+
explicitApiKey
|
|
391
|
+
};
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
function writeProcessorRunState(processor, input) {
|
|
395
|
+
const nextHistory = [
|
|
396
|
+
input.runEntry,
|
|
397
|
+
...processor.runHistory.filter((entry) => entry.id !== input.runEntry.id)
|
|
398
|
+
].slice(0, MAX_RUN_HISTORY);
|
|
399
|
+
getDatabase()
|
|
400
|
+
.prepare(`UPDATE ai_processors
|
|
401
|
+
SET last_run_at = ?, last_run_status = ?, last_run_output_json = ?, run_history_json = ?, updated_at = ?
|
|
402
|
+
WHERE id = ?`)
|
|
403
|
+
.run(input.lastRunAt, input.lastRunStatus, input.lastRunOutput ? JSON.stringify(input.lastRunOutput) : null, JSON.stringify(nextHistory), new Date().toISOString(), processor.id);
|
|
404
|
+
}
|
|
405
|
+
async function executeAiProcessor(processorId, input, services, state) {
|
|
406
|
+
if (state.cache.has(processorId)) {
|
|
407
|
+
return state.cache.get(processorId);
|
|
408
|
+
}
|
|
409
|
+
if (state.active.has(processorId)) {
|
|
410
|
+
throw new Error("Processor graph contains a cycle.");
|
|
411
|
+
}
|
|
412
|
+
const processor = getAiProcessorById(processorId);
|
|
413
|
+
if (!processor) {
|
|
414
|
+
throw new Error("AI processor not found.");
|
|
415
|
+
}
|
|
416
|
+
state.active.add(processorId);
|
|
417
|
+
const parsed = runAiProcessorSchema.parse(input);
|
|
418
|
+
const runEntryId = `air_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
|
|
419
|
+
const runStartedAt = new Date().toISOString();
|
|
420
|
+
writeProcessorRunState(processor, {
|
|
421
|
+
lastRunAt: runStartedAt,
|
|
422
|
+
lastRunStatus: "running",
|
|
423
|
+
lastRunOutput: processor.lastRunOutput,
|
|
424
|
+
runEntry: {
|
|
425
|
+
id: runEntryId,
|
|
426
|
+
trigger: state.trigger,
|
|
427
|
+
startedAt: runStartedAt,
|
|
428
|
+
completedAt: null,
|
|
429
|
+
status: "running",
|
|
430
|
+
input: parsed.input,
|
|
431
|
+
output: null,
|
|
432
|
+
error: null
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
try {
|
|
436
|
+
const links = listAiProcessorLinks(processor.surfaceId).filter((link) => link.targetProcessorId === processor.id);
|
|
437
|
+
const upstreamOutputs = [];
|
|
438
|
+
const linkedContext = [];
|
|
439
|
+
for (const link of links) {
|
|
440
|
+
const sourceProcessorId = processorIdFromNodeId(link.sourceWidgetId);
|
|
441
|
+
if (sourceProcessorId) {
|
|
442
|
+
const upstream = await executeAiProcessor(sourceProcessorId, {
|
|
443
|
+
input: parsed.input,
|
|
444
|
+
context: parsed.context,
|
|
445
|
+
widgetSnapshots: parsed.widgetSnapshots
|
|
446
|
+
}, services, state);
|
|
447
|
+
upstreamOutputs.push({
|
|
448
|
+
processorId: sourceProcessorId,
|
|
449
|
+
title: upstream.processor.title,
|
|
450
|
+
output: upstream.output
|
|
451
|
+
});
|
|
452
|
+
linkedContext.push(`Upstream processor ${upstream.processor.title} provided ${link.capabilityMode} access (${link.accessMode}).`);
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
const snapshot = parsed.widgetSnapshots[link.sourceWidgetId];
|
|
456
|
+
linkedContext.push([
|
|
457
|
+
`Linked widget ${link.sourceWidgetId} offers ${link.capabilityMode} access (${link.accessMode}).`,
|
|
458
|
+
snapshot !== undefined
|
|
459
|
+
? `Snapshot: ${JSON.stringify(snapshot)}`
|
|
460
|
+
: `Metadata: ${JSON.stringify(link.metadata)}`
|
|
461
|
+
].join(" "));
|
|
462
|
+
}
|
|
463
|
+
const fullPrompt = [
|
|
464
|
+
processor.promptFlow.trim(),
|
|
465
|
+
processor.contextInput.trim()
|
|
466
|
+
? `Processor context:\n${processor.contextInput.trim()}`
|
|
467
|
+
: "",
|
|
468
|
+
linkedContext.length > 0
|
|
469
|
+
? `Linked capabilities:\n${linkedContext.join("\n")}`
|
|
470
|
+
: "",
|
|
471
|
+
upstreamOutputs.length > 0
|
|
472
|
+
? `Upstream processor outputs:\n${upstreamOutputs
|
|
473
|
+
.map((entry) => `${entry.title} (${entry.processorId})\n${entry.output.concatenated}`)
|
|
474
|
+
.join("\n\n")}`
|
|
475
|
+
: "",
|
|
476
|
+
parsed.input.trim() ? `Runtime input:\n${parsed.input.trim()}` : "",
|
|
477
|
+
Object.keys(parsed.context).length > 0
|
|
478
|
+
? `Structured context:\n${JSON.stringify(parsed.context, null, 2)}`
|
|
479
|
+
: ""
|
|
480
|
+
]
|
|
481
|
+
.filter(Boolean)
|
|
482
|
+
.join("\n\n");
|
|
483
|
+
const agents = resolveProcessorAgentProfiles(processor, services.secrets);
|
|
484
|
+
const outputsByAgent = {};
|
|
485
|
+
await Promise.all(agents.map(async (agent) => {
|
|
486
|
+
outputsByAgent[agent.agentLabel] = await runProcessorAgent(processor, agent, fullPrompt, {
|
|
487
|
+
llm: services.llm
|
|
488
|
+
});
|
|
489
|
+
}));
|
|
490
|
+
const output = {
|
|
491
|
+
concatenated: Object.entries(outputsByAgent)
|
|
492
|
+
.map(([agentLabel, text]) => `${agentLabel}\n${text}`.trim())
|
|
493
|
+
.join("\n\n"),
|
|
494
|
+
byAgent: outputsByAgent
|
|
495
|
+
};
|
|
496
|
+
const completedAt = new Date().toISOString();
|
|
497
|
+
writeProcessorRunState(processor, {
|
|
498
|
+
lastRunAt: completedAt,
|
|
499
|
+
lastRunStatus: "completed",
|
|
500
|
+
lastRunOutput: output,
|
|
501
|
+
runEntry: {
|
|
502
|
+
id: runEntryId,
|
|
503
|
+
trigger: state.trigger,
|
|
504
|
+
startedAt: runStartedAt,
|
|
505
|
+
completedAt,
|
|
506
|
+
status: "completed",
|
|
507
|
+
input: parsed.input,
|
|
508
|
+
output,
|
|
509
|
+
error: null
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
const result = {
|
|
513
|
+
processor: getAiProcessorById(processor.id),
|
|
514
|
+
output
|
|
515
|
+
};
|
|
516
|
+
state.cache.set(processorId, result);
|
|
517
|
+
state.active.delete(processorId);
|
|
518
|
+
return result;
|
|
519
|
+
}
|
|
520
|
+
catch (error) {
|
|
521
|
+
const failedAt = new Date().toISOString();
|
|
522
|
+
writeProcessorRunState(processor, {
|
|
523
|
+
lastRunAt: failedAt,
|
|
524
|
+
lastRunStatus: "failed",
|
|
525
|
+
lastRunOutput: processor.lastRunOutput,
|
|
526
|
+
runEntry: {
|
|
527
|
+
id: runEntryId,
|
|
528
|
+
trigger: state.trigger,
|
|
529
|
+
startedAt: runStartedAt,
|
|
530
|
+
completedAt: failedAt,
|
|
531
|
+
status: "failed",
|
|
532
|
+
input: parsed.input,
|
|
533
|
+
output: null,
|
|
534
|
+
error: error instanceof Error ? error.message : "Processor run failed."
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
state.active.delete(processorId);
|
|
538
|
+
throw error;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
export async function runAiProcessor(processorId, input, services, options = {}) {
|
|
542
|
+
return await executeAiProcessor(processorId, input, services, {
|
|
543
|
+
cache: new Map(),
|
|
544
|
+
active: new Set(),
|
|
545
|
+
trigger: options.trigger ?? "manual"
|
|
546
|
+
});
|
|
547
|
+
}
|
|
@@ -1132,7 +1132,7 @@ export function getCalendarOverview(query) {
|
|
|
1132
1132
|
provider: "google",
|
|
1133
1133
|
label: "Google Calendar",
|
|
1134
1134
|
supportsDedicatedForgeCalendar: true,
|
|
1135
|
-
connectionHelp: "
|
|
1135
|
+
connectionHelp: "Forge uses a localhost Authorization Code + PKCE flow. Users sign in with Google from the same machine running Forge, Forge exchanges the code on the backend, and stores a per-user refresh token server-side."
|
|
1136
1136
|
},
|
|
1137
1137
|
{
|
|
1138
1138
|
provider: "apple",
|
|
@@ -88,5 +88,13 @@ export function filterOwnedEntities(entityType, entities, userIds) {
|
|
|
88
88
|
return decorated;
|
|
89
89
|
}
|
|
90
90
|
const allowed = new Set(userIds);
|
|
91
|
-
return decorated.filter((entity) =>
|
|
91
|
+
return decorated.filter((entity) => {
|
|
92
|
+
if (entity.userId !== null && allowed.has(entity.userId)) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
const embeddedUserId = "userId" in entity && typeof entity.userId === "string"
|
|
96
|
+
? entity.userId
|
|
97
|
+
: null;
|
|
98
|
+
return embeddedUserId !== null && allowed.has(embeddedUserId);
|
|
99
|
+
});
|
|
92
100
|
}
|
|
@@ -77,13 +77,72 @@ function calculateCompletionRate(habit, checkIns) {
|
|
|
77
77
|
const aligned = checkIns.filter((checkIn) => isAligned(habit, checkIn)).length;
|
|
78
78
|
return Math.round((aligned / checkIns.length) * 100);
|
|
79
79
|
}
|
|
80
|
-
function calculateStreak(habit, checkIns) {
|
|
81
|
-
|
|
80
|
+
function calculateStreak(habit, checkIns, now = new Date()) {
|
|
81
|
+
if (habit.frequency === "weekly" && habit.weekDays.length === 0) {
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
const statusByDate = new Map();
|
|
82
85
|
for (const checkIn of checkIns) {
|
|
83
|
-
if (!
|
|
84
|
-
|
|
86
|
+
if (!statusByDate.has(checkIn.dateKey)) {
|
|
87
|
+
statusByDate.set(checkIn.dateKey, checkIn.status);
|
|
85
88
|
}
|
|
89
|
+
}
|
|
90
|
+
const isScheduledOn = (date) => habit.frequency === "daily" || habit.weekDays.includes(date.getUTCDay());
|
|
91
|
+
const toDateKey = (date) => date.toISOString().slice(0, 10);
|
|
92
|
+
const atUtcDayStart = (date) => new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
|
93
|
+
const previousScheduledDate = (date) => {
|
|
94
|
+
const cursor = atUtcDayStart(date);
|
|
95
|
+
do {
|
|
96
|
+
cursor.setUTCDate(cursor.getUTCDate() - 1);
|
|
97
|
+
} while (!isScheduledOn(cursor));
|
|
98
|
+
return cursor;
|
|
99
|
+
};
|
|
100
|
+
const startOfUtcWeek = (date) => {
|
|
101
|
+
const start = atUtcDayStart(date);
|
|
102
|
+
const offset = (start.getUTCDay() + 6) % 7;
|
|
103
|
+
start.setUTCDate(start.getUTCDate() - offset);
|
|
104
|
+
return start;
|
|
105
|
+
};
|
|
106
|
+
const previousUtcWeek = (date) => {
|
|
107
|
+
const start = startOfUtcWeek(date);
|
|
108
|
+
start.setUTCDate(start.getUTCDate() - 7);
|
|
109
|
+
return start;
|
|
110
|
+
};
|
|
111
|
+
const alignedStatusOn = (date) => {
|
|
112
|
+
const status = statusByDate.get(toDateKey(date));
|
|
113
|
+
return status ? isAligned(habit, { status }) : false;
|
|
114
|
+
};
|
|
115
|
+
if (habit.frequency === "daily") {
|
|
116
|
+
const today = atUtcDayStart(now);
|
|
117
|
+
let cursor = isScheduledOn(today) && !statusByDate.has(toDateKey(today))
|
|
118
|
+
? previousScheduledDate(today)
|
|
119
|
+
: today;
|
|
120
|
+
let streak = 0;
|
|
121
|
+
while (alignedStatusOn(cursor)) {
|
|
122
|
+
streak += 1;
|
|
123
|
+
cursor = previousScheduledDate(cursor);
|
|
124
|
+
}
|
|
125
|
+
return streak;
|
|
126
|
+
}
|
|
127
|
+
const alignedCountForWeek = (weekStart) => {
|
|
128
|
+
let count = 0;
|
|
129
|
+
for (let offset = 0; offset < 7; offset += 1) {
|
|
130
|
+
const day = new Date(weekStart);
|
|
131
|
+
day.setUTCDate(weekStart.getUTCDate() + offset);
|
|
132
|
+
if (isScheduledOn(day) && alignedStatusOn(day)) {
|
|
133
|
+
count += 1;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return count;
|
|
137
|
+
};
|
|
138
|
+
const currentWeekStart = startOfUtcWeek(now);
|
|
139
|
+
let cursor = alignedCountForWeek(currentWeekStart) >= habit.targetCount
|
|
140
|
+
? currentWeekStart
|
|
141
|
+
: previousUtcWeek(currentWeekStart);
|
|
142
|
+
let streak = 0;
|
|
143
|
+
while (alignedCountForWeek(cursor) >= habit.targetCount) {
|
|
86
144
|
streak += 1;
|
|
145
|
+
cursor = previousUtcWeek(cursor);
|
|
87
146
|
}
|
|
88
147
|
return streak;
|
|
89
148
|
}
|
|
@@ -137,7 +196,12 @@ function mapHabit(row, checkIns = listCheckInsForHabit(row.id)) {
|
|
|
137
196
|
updatedAt: row.updated_at,
|
|
138
197
|
lastCheckInAt: latestCheckIn?.createdAt ?? null,
|
|
139
198
|
lastCheckInStatus: latestCheckIn?.status ?? null,
|
|
140
|
-
streakCount: calculateStreak({
|
|
199
|
+
streakCount: calculateStreak({
|
|
200
|
+
polarity: row.polarity,
|
|
201
|
+
frequency: row.frequency,
|
|
202
|
+
targetCount: row.target_count,
|
|
203
|
+
weekDays: parseWeekDays(row.week_days_json)
|
|
204
|
+
}, checkIns),
|
|
141
205
|
completionRate: calculateCompletionRate({ polarity: row.polarity }, checkIns),
|
|
142
206
|
dueToday: false,
|
|
143
207
|
checkIns
|