ofiere-openclaw-plugin 4.52.0 → 4.54.0
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/src/tools.js +606 -12
- package/package.json +2 -2
- package/src/tools.ts +627 -13
package/src/tools.ts
CHANGED
|
@@ -271,7 +271,7 @@ function registerTaskOps(
|
|
|
271
271
|
`Actions:\n` +
|
|
272
272
|
`- "list": List/filter tasks. Optional: status, agent_id, space_id, folder_id, task_id, limit\n` +
|
|
273
273
|
`- "get": Get a single task by ID. Required: task_id\n` +
|
|
274
|
-
`- "create": Create a task. Required: title. Optional: agent_id, subagent_id, description, status, priority, space_id, folder_id, start_date, due_date, tags, instructions, execution_plan, goals, constraints, system_prompt, recurrence_type, recurrence_interval, scheduled_time. ⚠️ PLANNING GATE: If the task has 3+ execution steps, goals, or constraints, FIRST ask the user "Plan first or create directly?" If they choose to plan, use OFIERE_PLAN_OPS instead.\n` +
|
|
274
|
+
`- "create": Create a task in DRAFT mode. Required: title. Optional: agent_id, subagent_id, description, status, priority, space_id, folder_id, start_date, due_date, dispatch_immediately, tags, instructions, execution_plan, goals, constraints, system_prompt, recurrence_type, recurrence_interval, scheduled_time. ⚠️ EXECUTION SEMANTICS: omit start_date → task sits in agent's task list as draft (does NOT run). Set start_date to schedule. Pass dispatch_immediately:true to run now. ⚠️ PLANNING GATE: If the task has 3+ execution steps, goals, or constraints, FIRST ask the user "Plan first or create directly?" If they choose to plan, use OFIERE_PLAN_OPS instead.\n` +
|
|
275
275
|
`- "update": Update a task. Required: task_id. Optional: all create fields + progress + subagent_id\n` +
|
|
276
276
|
`- "delete": Delete task + subtasks. Required: task_id\n` +
|
|
277
277
|
`- "add_approval": Request approval on a task. Required: task_id, approver_name. Optional: approver_type (human|agent, auto-detected), due_date, comment\n` +
|
|
@@ -317,8 +317,12 @@ function registerTaskOps(
|
|
|
317
317
|
progress: { type: "number", description: "Progress percentage 0-100 (update only)" },
|
|
318
318
|
space_id: { type: "string", description: "PM Space ID" },
|
|
319
319
|
folder_id: { type: "string", description: "PM Folder ID" },
|
|
320
|
-
start_date: { type: "string", description: "Start date (ISO 8601). Required for scheduled/recurring tasks." },
|
|
320
|
+
start_date: { type: "string", description: "Start date (ISO 8601). Required for scheduled/recurring tasks. Omit to create as draft." },
|
|
321
321
|
due_date: { type: "string", description: "Due date (ISO 8601)" },
|
|
322
|
+
dispatch_immediately: {
|
|
323
|
+
type: "boolean",
|
|
324
|
+
description: "When true and start_date is not provided, sets start_date to NOW so the task dispatches on the next cron tick. Default: false (task is created as draft and waits for an explicit start_date).",
|
|
325
|
+
},
|
|
322
326
|
scheduled_time: { type: "string", description: "Time to execute in HH:MM format (user's LOCAL time, e.g. 10:00 for 10 AM WIB). The system converts to UTC automatically." },
|
|
323
327
|
recurrence_type: {
|
|
324
328
|
type: "string",
|
|
@@ -749,15 +753,17 @@ async function handleCreateTask(
|
|
|
749
753
|
// which left the DB row with the unconverted value and the response with
|
|
750
754
|
// the converted one — confusing and unsafe for the scheduler.
|
|
751
755
|
//
|
|
752
|
-
// Cycle
|
|
753
|
-
//
|
|
754
|
-
//
|
|
755
|
-
//
|
|
756
|
-
//
|
|
757
|
-
//
|
|
756
|
+
// Cycle 13 (BUG 3+4): create is a DRAFT operation, NOT a dispatch trigger.
|
|
757
|
+
// No start_date → rawStartDate stays null → no scheduler_event auto-insert
|
|
758
|
+
// → task sits in agent's task list as draft. To execute, caller must pass
|
|
759
|
+
// an explicit start_date OR set dispatch_immediately:true (which we then
|
|
760
|
+
// resolve to "now" ISO here). Cycle 12's silent +5s default for staff
|
|
761
|
+
// delegation is REMOVED — it turned create into an unsafe execution
|
|
762
|
+
// trigger and bypassed the chief's approval flow.
|
|
758
763
|
const explicitStartDate = (params.start_date as string) || null;
|
|
764
|
+
const dispatchNow = params.dispatch_immediately === true;
|
|
759
765
|
const rawStartDate = explicitStartDate
|
|
760
|
-
?? (
|
|
766
|
+
?? (dispatchNow ? new Date().toISOString() : null);
|
|
761
767
|
const normalized = rawStartDate
|
|
762
768
|
? normalizeStartDate(rawStartDate, params.scheduled_time as string | undefined, TZ_OFFSET_HOURS)
|
|
763
769
|
: null;
|
|
@@ -896,7 +902,16 @@ async function handleUpdateTask(
|
|
|
896
902
|
for (const f of fields) {
|
|
897
903
|
if (params[f] !== undefined) updates[f] = params[f];
|
|
898
904
|
}
|
|
899
|
-
|
|
905
|
+
// Cycle 13 (BUG 2): keep status and completed_at consistent. Setting status
|
|
906
|
+
// to DONE stamps completed_at + progress=100. Setting status to anything
|
|
907
|
+
// other than DONE clears completed_at so the row never carries the
|
|
908
|
+
// "PENDING + completed_at" contradiction the dashboard reported.
|
|
909
|
+
if (params.status === "DONE") {
|
|
910
|
+
updates.completed_at = new Date().toISOString();
|
|
911
|
+
if (params.progress === undefined) updates.progress = 100;
|
|
912
|
+
} else if (typeof params.status === "string") {
|
|
913
|
+
updates.completed_at = null;
|
|
914
|
+
}
|
|
900
915
|
|
|
901
916
|
// ── Subagent + chief invariant ───────────────────────────────────────
|
|
902
917
|
// Mirrors dashboard PATCH: chief change without explicit subagent_id
|
|
@@ -2941,13 +2956,72 @@ async function generatePMReportDirect(
|
|
|
2941
2956
|
lines.push("");
|
|
2942
2957
|
lines.push(`💡 Reply "details on #N" for full task result`);
|
|
2943
2958
|
|
|
2959
|
+
// Cycle 13 (BUG 1): render the "Generated" stamp in the user's IANA TZ
|
|
2960
|
+
// (from profiles.timezone) instead of UTC. The plugin can't import
|
|
2961
|
+
// dashboard helpers, so this uses Intl.DateTimeFormat directly.
|
|
2962
|
+
const userTz = await loadUserTimezoneForPlugin(supabase, userId);
|
|
2944
2963
|
const now = new Date();
|
|
2945
2964
|
lines.push("");
|
|
2946
|
-
lines.push(`Generated: ${now
|
|
2965
|
+
lines.push(`Generated: ${formatInUserTzInline(now, userTz)}`);
|
|
2947
2966
|
|
|
2948
2967
|
return { report: lines.join("\n"), scopeLabel, taskCount: all.length };
|
|
2949
2968
|
}
|
|
2950
2969
|
|
|
2970
|
+
// ─── Plugin-local TZ helpers (Cycle 13, BUG 1) ──────────────────────────────
|
|
2971
|
+
// Mirror dashboard/lib/tz.ts; kept inline because the plugin is a separate
|
|
2972
|
+
// npm package loaded directly by the OpenClaw gateway and cannot import from
|
|
2973
|
+
// the dashboard.
|
|
2974
|
+
|
|
2975
|
+
const PLUGIN_FALLBACK_TZ = "Asia/Jakarta";
|
|
2976
|
+
|
|
2977
|
+
async function loadUserTimezoneForPlugin(
|
|
2978
|
+
supabase: SupabaseClient,
|
|
2979
|
+
userId: string,
|
|
2980
|
+
): Promise<string> {
|
|
2981
|
+
if (!userId) return PLUGIN_FALLBACK_TZ;
|
|
2982
|
+
try {
|
|
2983
|
+
const { data } = await supabase
|
|
2984
|
+
.from("profiles")
|
|
2985
|
+
.select("timezone")
|
|
2986
|
+
.eq("id", userId)
|
|
2987
|
+
.maybeSingle();
|
|
2988
|
+
const tz = ((data as any)?.timezone as string | undefined)?.trim();
|
|
2989
|
+
return tz && isValidIanaZoneInline(tz) ? tz : PLUGIN_FALLBACK_TZ;
|
|
2990
|
+
} catch {
|
|
2991
|
+
return PLUGIN_FALLBACK_TZ;
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
|
|
2995
|
+
function formatInUserTzInline(date: Date, timezone: string): string {
|
|
2996
|
+
if (Number.isNaN(date.getTime())) return "";
|
|
2997
|
+
const tz = isValidIanaZoneInline(timezone) ? timezone : PLUGIN_FALLBACK_TZ;
|
|
2998
|
+
try {
|
|
2999
|
+
const fmt = new Intl.DateTimeFormat("en-US", {
|
|
3000
|
+
timeZone: tz,
|
|
3001
|
+
year: "numeric",
|
|
3002
|
+
month: "short",
|
|
3003
|
+
day: "numeric",
|
|
3004
|
+
hour: "2-digit",
|
|
3005
|
+
minute: "2-digit",
|
|
3006
|
+
hour12: false,
|
|
3007
|
+
timeZoneName: "short",
|
|
3008
|
+
});
|
|
3009
|
+
return fmt.format(date).replace(", ", " ");
|
|
3010
|
+
} catch {
|
|
3011
|
+
return date.toISOString();
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
function isValidIanaZoneInline(tz: string | undefined | null): boolean {
|
|
3016
|
+
if (!tz) return false;
|
|
3017
|
+
try {
|
|
3018
|
+
new Intl.DateTimeFormat("en-US", { timeZone: tz }).format(new Date());
|
|
3019
|
+
return true;
|
|
3020
|
+
} catch {
|
|
3021
|
+
return false;
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
|
|
2951
3025
|
async function dispatchReportDirect(
|
|
2952
3026
|
supabase: SupabaseClient,
|
|
2953
3027
|
userId: string,
|
|
@@ -6525,6 +6599,453 @@ function registerTalentContextHook(
|
|
|
6525
6599
|
}
|
|
6526
6600
|
}
|
|
6527
6601
|
|
|
6602
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
6603
|
+
// META-TOOL 17: OFIERE_OFFICE_OPS — Agent Office Canvas (widget dashboard)
|
|
6604
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
6605
|
+
|
|
6606
|
+
async function emitOfficeWebhook(
|
|
6607
|
+
api: any,
|
|
6608
|
+
type: "office_update" | "office_data_update",
|
|
6609
|
+
payload: Record<string, unknown>,
|
|
6610
|
+
): Promise<void> {
|
|
6611
|
+
const webhookUrl = process.env.OFIERE_DASHBOARD_WEBHOOK_URL || "";
|
|
6612
|
+
const webhookSecret = process.env.OPENCLAW_WEBHOOK_SECRET || "";
|
|
6613
|
+
if (!webhookUrl || !webhookSecret) return;
|
|
6614
|
+
try {
|
|
6615
|
+
await fetch(webhookUrl, {
|
|
6616
|
+
method: "POST",
|
|
6617
|
+
headers: {
|
|
6618
|
+
"content-type": "application/json",
|
|
6619
|
+
authorization: `Bearer ${webhookSecret}`,
|
|
6620
|
+
},
|
|
6621
|
+
body: JSON.stringify({ type, payload }),
|
|
6622
|
+
});
|
|
6623
|
+
} catch (wErr) {
|
|
6624
|
+
api.logger.debug?.(`[ofiere-office] webhook post failed: ${wErr instanceof Error ? wErr.message : String(wErr)}`);
|
|
6625
|
+
}
|
|
6626
|
+
}
|
|
6627
|
+
|
|
6628
|
+
function validateSection(s: any): string | null {
|
|
6629
|
+
if (!s || typeof s !== "object") return "section is not an object";
|
|
6630
|
+
if (typeof s.title !== "string" || !s.title) return "section.title required";
|
|
6631
|
+
if (typeof s.order !== "number") return "section.order must be a number";
|
|
6632
|
+
if (!Array.isArray(s.widgets)) return "section.widgets must be an array";
|
|
6633
|
+
for (const w of s.widgets) {
|
|
6634
|
+
const wErr = validateWidget(w);
|
|
6635
|
+
if (wErr) return `section "${s.title}": ${wErr}`;
|
|
6636
|
+
}
|
|
6637
|
+
return null;
|
|
6638
|
+
}
|
|
6639
|
+
|
|
6640
|
+
function validateWidget(w: any): string | null {
|
|
6641
|
+
if (!w || typeof w !== "object") return "widget is not an object";
|
|
6642
|
+
if (typeof w.type !== "string" || !w.type) return "widget.type required";
|
|
6643
|
+
if (typeof w.config !== "object" || w.config === null) return "widget.config must be an object";
|
|
6644
|
+
const ly = w.layout;
|
|
6645
|
+
if (!ly || typeof ly !== "object") return "widget.layout required";
|
|
6646
|
+
if (typeof ly.x !== "number" || typeof ly.y !== "number" || typeof ly.w !== "number" || typeof ly.h !== "number") {
|
|
6647
|
+
return "widget.layout must have numeric x, y, w, h";
|
|
6648
|
+
}
|
|
6649
|
+
return null;
|
|
6650
|
+
}
|
|
6651
|
+
|
|
6652
|
+
function shortId(prefix: string): string {
|
|
6653
|
+
return `${prefix}_${Math.random().toString(36).slice(2, 10)}`;
|
|
6654
|
+
}
|
|
6655
|
+
|
|
6656
|
+
function registerOfficeOps(
|
|
6657
|
+
api: any,
|
|
6658
|
+
supabase: SupabaseClient,
|
|
6659
|
+
userId: string,
|
|
6660
|
+
resolveAgent: (id?: string) => Promise<string | null>,
|
|
6661
|
+
): void {
|
|
6662
|
+
api.registerTool((toolCtx: any) => ({
|
|
6663
|
+
name: "OFIERE_OFFICE_OPS",
|
|
6664
|
+
label: "Ofiere Office Operations",
|
|
6665
|
+
description:
|
|
6666
|
+
`Build and update your department Office — a widget-based dashboard canvas bound to a PM Space.\n\n` +
|
|
6667
|
+
`Actions:\n` +
|
|
6668
|
+
`- "get_office": Load the current layout + widget data for a given (space_id, agent_id). Optional: space_id, agent_id.\n` +
|
|
6669
|
+
`- "build_office": One-time initial build. Required: space_id, sections. Only callable while the office row is in 'initializing' state.\n` +
|
|
6670
|
+
`- "update_widgets": Modify the layout (add/remove/replace/patch widgets or sections). Required: office_id, operations. Locked items are skipped (returns warnings).\n` +
|
|
6671
|
+
`- "update_widget_data": Push fresh data for a single widget. Required: office_id, widget_id, data. Always succeeds regardless of lock state — locks protect layout, not data.\n\n` +
|
|
6672
|
+
`Layout shape:\n` +
|
|
6673
|
+
` { version: 1, sections: [ { id, title, description?, order, locked?, collapsed?, columns, widgets: [ { id, type, config, layout: {x,y,w,h}, locked?, data_source? } ] } ] }\n\n` +
|
|
6674
|
+
`Widget catalog (the dashboard knows which types exist): kpi_card, bar_chart, line_chart, donut_chart, progress_bar, sparkline, heatmap, data_table, activity_feed, report_card, status_list, checklist, text_block, embed, quick_action, file_browser, staff_roster.\n\n` +
|
|
6675
|
+
`Operation shape (RFC-6902 flavored): { op: 'add'|'remove'|'replace'|'patch', path: '/sections/0/widgets/-' or '/sections/0/widgets/0', value?, widget?, config? }`,
|
|
6676
|
+
parameters: {
|
|
6677
|
+
type: "object",
|
|
6678
|
+
required: ["action"],
|
|
6679
|
+
properties: {
|
|
6680
|
+
action: {
|
|
6681
|
+
type: "string",
|
|
6682
|
+
description: "get_office, build_office, update_widgets, update_widget_data",
|
|
6683
|
+
},
|
|
6684
|
+
space_id: { type: "string", description: "PM space UUID" },
|
|
6685
|
+
agent_id: { type: "string", description: "Agent identifier (defaults to calling agent)" },
|
|
6686
|
+
office_id: { type: "string", description: "Office row UUID" },
|
|
6687
|
+
widget_id: { type: "string", description: "Widget short id" },
|
|
6688
|
+
sections: { type: "array", description: "Initial layout sections (build_office only)" },
|
|
6689
|
+
operations: { type: "array", description: "Patch operations (update_widgets only)" },
|
|
6690
|
+
data: { type: "object", description: "Widget data payload (update_widget_data only)" },
|
|
6691
|
+
},
|
|
6692
|
+
},
|
|
6693
|
+
async execute(_id: string, params: Record<string, unknown>) {
|
|
6694
|
+
const action = params.action as string;
|
|
6695
|
+
const ctxAgentHint = toolCtx?.agentAccountId || toolCtx?.agentId || "";
|
|
6696
|
+
|
|
6697
|
+
async function resolveCallingAgent(explicitId?: string): Promise<string | null> {
|
|
6698
|
+
if (explicitId && explicitId.trim()) return resolveAgent(explicitId);
|
|
6699
|
+
if (ctxAgentHint) return resolveAgent(ctxAgentHint);
|
|
6700
|
+
return resolveAgent();
|
|
6701
|
+
}
|
|
6702
|
+
|
|
6703
|
+
switch (action) {
|
|
6704
|
+
case "get_office": {
|
|
6705
|
+
const agentId = await resolveCallingAgent(params.agent_id as string);
|
|
6706
|
+
if (!agentId) return err("Could not resolve agent_id");
|
|
6707
|
+
|
|
6708
|
+
let q = supabase
|
|
6709
|
+
.from("agent_office_layouts")
|
|
6710
|
+
.select("id, space_id, agent_id, status, is_finalized, layout, last_agent_update_at, error_message, updated_at")
|
|
6711
|
+
.eq("user_id", userId)
|
|
6712
|
+
.eq("agent_id", agentId);
|
|
6713
|
+
if (params.space_id) q = q.eq("space_id", params.space_id as string);
|
|
6714
|
+
|
|
6715
|
+
const { data: rows, error } = await q.limit(1);
|
|
6716
|
+
if (error) return err(error.message);
|
|
6717
|
+
if (!rows || rows.length === 0) return err("No office found for this agent");
|
|
6718
|
+
|
|
6719
|
+
const office = rows[0];
|
|
6720
|
+
const { data: widgetData } = await supabase
|
|
6721
|
+
.from("agent_office_data")
|
|
6722
|
+
.select("widget_id, data, updated_at")
|
|
6723
|
+
.eq("user_id", userId)
|
|
6724
|
+
.eq("office_id", office.id);
|
|
6725
|
+
|
|
6726
|
+
const dataMap: Record<string, any> = {};
|
|
6727
|
+
for (const row of widgetData || []) {
|
|
6728
|
+
dataMap[row.widget_id] = row.data;
|
|
6729
|
+
}
|
|
6730
|
+
|
|
6731
|
+
return ok({ office, widget_data: dataMap });
|
|
6732
|
+
}
|
|
6733
|
+
|
|
6734
|
+
case "build_office": {
|
|
6735
|
+
if (!params.space_id) return err("Missing required: space_id");
|
|
6736
|
+
if (!Array.isArray(params.sections)) return err("Missing required: sections (array)");
|
|
6737
|
+
|
|
6738
|
+
const agentId = await resolveCallingAgent(params.agent_id as string);
|
|
6739
|
+
if (!agentId) return err("Could not resolve agent_id");
|
|
6740
|
+
|
|
6741
|
+
const inputSections = params.sections as any[];
|
|
6742
|
+
for (let i = 0; i < inputSections.length; i++) {
|
|
6743
|
+
const sErr = validateSection(inputSections[i]);
|
|
6744
|
+
if (sErr) return err(`Validation failed at section[${i}]: ${sErr}`);
|
|
6745
|
+
}
|
|
6746
|
+
|
|
6747
|
+
const { data: existing, error: loadErr } = await supabase
|
|
6748
|
+
.from("agent_office_layouts")
|
|
6749
|
+
.select("id, status")
|
|
6750
|
+
.eq("user_id", userId)
|
|
6751
|
+
.eq("space_id", params.space_id as string)
|
|
6752
|
+
.eq("agent_id", agentId)
|
|
6753
|
+
.maybeSingle();
|
|
6754
|
+
|
|
6755
|
+
if (loadErr) return err(`Office lookup failed: ${loadErr.message}`);
|
|
6756
|
+
if (!existing) return err("Office row not found — dashboard must call /api/office/initialize first");
|
|
6757
|
+
if (existing.status !== "initializing") {
|
|
6758
|
+
return err(`Office already built (status=${existing.status}). Use update_widgets to modify.`);
|
|
6759
|
+
}
|
|
6760
|
+
|
|
6761
|
+
const normalized = inputSections.map((s, idx) => {
|
|
6762
|
+
const widgets = (s.widgets || []).map((w: any) => ({
|
|
6763
|
+
id: typeof w.id === "string" && w.id ? w.id : shortId("wgt"),
|
|
6764
|
+
type: w.type,
|
|
6765
|
+
config: w.config || {},
|
|
6766
|
+
layout: {
|
|
6767
|
+
x: w.layout.x,
|
|
6768
|
+
y: w.layout.y,
|
|
6769
|
+
w: w.layout.w,
|
|
6770
|
+
h: w.layout.h,
|
|
6771
|
+
minW: w.layout.minW,
|
|
6772
|
+
minH: w.layout.minH,
|
|
6773
|
+
maxW: w.layout.maxW,
|
|
6774
|
+
maxH: w.layout.maxH,
|
|
6775
|
+
},
|
|
6776
|
+
locked: w.locked === true,
|
|
6777
|
+
data_source: w.data_source || { type: "agent", refresh_interval: 3600, last_refreshed_at: null },
|
|
6778
|
+
}));
|
|
6779
|
+
return {
|
|
6780
|
+
id: typeof s.id === "string" && s.id ? s.id : shortId("sec"),
|
|
6781
|
+
title: s.title,
|
|
6782
|
+
description: s.description,
|
|
6783
|
+
order: typeof s.order === "number" ? s.order : idx,
|
|
6784
|
+
locked: s.locked === true,
|
|
6785
|
+
collapsed: s.collapsed === true,
|
|
6786
|
+
columns: typeof s.columns === "number" ? s.columns : 3,
|
|
6787
|
+
widgets,
|
|
6788
|
+
};
|
|
6789
|
+
});
|
|
6790
|
+
|
|
6791
|
+
const layout = { version: 1, sections: normalized };
|
|
6792
|
+
|
|
6793
|
+
const { error: updateErr } = await supabase
|
|
6794
|
+
.from("agent_office_layouts")
|
|
6795
|
+
.update({
|
|
6796
|
+
layout,
|
|
6797
|
+
status: "active",
|
|
6798
|
+
last_agent_update_at: new Date().toISOString(),
|
|
6799
|
+
updated_at: new Date().toISOString(),
|
|
6800
|
+
})
|
|
6801
|
+
.eq("id", existing.id)
|
|
6802
|
+
.eq("user_id", userId);
|
|
6803
|
+
|
|
6804
|
+
if (updateErr) return err(`Office save failed: ${updateErr.message}`);
|
|
6805
|
+
|
|
6806
|
+
const dataRows = normalized.flatMap((s: any) =>
|
|
6807
|
+
s.widgets.map((w: any) => ({
|
|
6808
|
+
user_id: userId,
|
|
6809
|
+
office_id: existing.id,
|
|
6810
|
+
widget_id: w.id,
|
|
6811
|
+
data: {},
|
|
6812
|
+
})),
|
|
6813
|
+
);
|
|
6814
|
+
|
|
6815
|
+
if (dataRows.length > 0) {
|
|
6816
|
+
const { error: dataErr } = await supabase
|
|
6817
|
+
.from("agent_office_data")
|
|
6818
|
+
.upsert(dataRows, { onConflict: "office_id,widget_id" });
|
|
6819
|
+
if (dataErr) {
|
|
6820
|
+
api.logger.warn?.(`[ofiere-office] widget_data seed failed: ${dataErr.message}`);
|
|
6821
|
+
}
|
|
6822
|
+
}
|
|
6823
|
+
|
|
6824
|
+
await emitOfficeWebhook(api, "office_update", {
|
|
6825
|
+
user_id: userId,
|
|
6826
|
+
office_id: existing.id,
|
|
6827
|
+
agent_id: agentId,
|
|
6828
|
+
});
|
|
6829
|
+
|
|
6830
|
+
return ok({
|
|
6831
|
+
message: `Office built with ${normalized.length} sections and ${dataRows.length} widgets`,
|
|
6832
|
+
office_id: existing.id,
|
|
6833
|
+
section_count: normalized.length,
|
|
6834
|
+
widget_count: dataRows.length,
|
|
6835
|
+
});
|
|
6836
|
+
}
|
|
6837
|
+
|
|
6838
|
+
case "update_widgets": {
|
|
6839
|
+
if (!params.office_id) return err("Missing required: office_id");
|
|
6840
|
+
if (!Array.isArray(params.operations)) return err("Missing required: operations (array)");
|
|
6841
|
+
const operations = params.operations as any[];
|
|
6842
|
+
|
|
6843
|
+
const agentId = await resolveCallingAgent(params.agent_id as string);
|
|
6844
|
+
if (!agentId) return err("Could not resolve agent_id");
|
|
6845
|
+
|
|
6846
|
+
const { data: office, error: loadErr } = await supabase
|
|
6847
|
+
.from("agent_office_layouts")
|
|
6848
|
+
.select("id, layout, is_finalized, agent_id")
|
|
6849
|
+
.eq("id", params.office_id as string)
|
|
6850
|
+
.eq("user_id", userId)
|
|
6851
|
+
.eq("agent_id", agentId)
|
|
6852
|
+
.maybeSingle();
|
|
6853
|
+
|
|
6854
|
+
if (loadErr) return err(loadErr.message);
|
|
6855
|
+
if (!office) return err("Office not found or not owned by this agent");
|
|
6856
|
+
|
|
6857
|
+
const layout: any = office.layout || { version: 1, sections: [] };
|
|
6858
|
+
const sections: any[] = Array.isArray(layout.sections) ? [...layout.sections] : [];
|
|
6859
|
+
const warnings: string[] = [];
|
|
6860
|
+
const newSeedDataRows: { widget_id: string }[] = [];
|
|
6861
|
+
let applied = 0;
|
|
6862
|
+
let skipped = 0;
|
|
6863
|
+
|
|
6864
|
+
for (let i = 0; i < operations.length; i++) {
|
|
6865
|
+
const op = operations[i];
|
|
6866
|
+
const path: string = op?.path || "";
|
|
6867
|
+
const opType: string = op?.op || "";
|
|
6868
|
+
|
|
6869
|
+
const m = path.match(/^\/sections\/(\d+)(?:\/widgets\/(-|\d+))?(?:\/config)?$/);
|
|
6870
|
+
if (!m) {
|
|
6871
|
+
warnings.push(`op[${i}]: unsupported path "${path}"`);
|
|
6872
|
+
skipped++;
|
|
6873
|
+
continue;
|
|
6874
|
+
}
|
|
6875
|
+
|
|
6876
|
+
const sIdx = parseInt(m[1], 10);
|
|
6877
|
+
const wTok = m[2];
|
|
6878
|
+
const isConfig = path.endsWith("/config");
|
|
6879
|
+
|
|
6880
|
+
if (Number.isNaN(sIdx) || sIdx < 0 || sIdx >= sections.length) {
|
|
6881
|
+
warnings.push(`op[${i}]: section index ${sIdx} out of range`);
|
|
6882
|
+
skipped++;
|
|
6883
|
+
continue;
|
|
6884
|
+
}
|
|
6885
|
+
const section = sections[sIdx];
|
|
6886
|
+
if (section.locked) {
|
|
6887
|
+
warnings.push(`op[${i}]: section "${section.title}" is locked`);
|
|
6888
|
+
skipped++;
|
|
6889
|
+
continue;
|
|
6890
|
+
}
|
|
6891
|
+
|
|
6892
|
+
if (wTok === undefined) {
|
|
6893
|
+
if (opType === "remove") {
|
|
6894
|
+
sections.splice(sIdx, 1);
|
|
6895
|
+
applied++;
|
|
6896
|
+
continue;
|
|
6897
|
+
}
|
|
6898
|
+
if (opType === "replace" && op.value) {
|
|
6899
|
+
const sErr = validateSection(op.value);
|
|
6900
|
+
if (sErr) { warnings.push(`op[${i}]: ${sErr}`); skipped++; continue; }
|
|
6901
|
+
sections[sIdx] = { ...op.value, id: section.id };
|
|
6902
|
+
applied++;
|
|
6903
|
+
continue;
|
|
6904
|
+
}
|
|
6905
|
+
warnings.push(`op[${i}]: section-level op "${opType}" not supported (use replace/remove)`);
|
|
6906
|
+
skipped++;
|
|
6907
|
+
continue;
|
|
6908
|
+
}
|
|
6909
|
+
|
|
6910
|
+
if (wTok === "-") {
|
|
6911
|
+
if (opType !== "add") {
|
|
6912
|
+
warnings.push(`op[${i}]: path "/widgets/-" requires op=add`);
|
|
6913
|
+
skipped++;
|
|
6914
|
+
continue;
|
|
6915
|
+
}
|
|
6916
|
+
const w = op.widget || op.value;
|
|
6917
|
+
const wErr = validateWidget(w);
|
|
6918
|
+
if (wErr) { warnings.push(`op[${i}]: ${wErr}`); skipped++; continue; }
|
|
6919
|
+
const newW = {
|
|
6920
|
+
id: typeof w.id === "string" && w.id ? w.id : shortId("wgt"),
|
|
6921
|
+
type: w.type,
|
|
6922
|
+
config: w.config || {},
|
|
6923
|
+
layout: { ...w.layout },
|
|
6924
|
+
locked: w.locked === true,
|
|
6925
|
+
data_source: w.data_source || { type: "agent", refresh_interval: 3600, last_refreshed_at: null },
|
|
6926
|
+
};
|
|
6927
|
+
section.widgets = [...(section.widgets || []), newW];
|
|
6928
|
+
newSeedDataRows.push({ widget_id: newW.id });
|
|
6929
|
+
applied++;
|
|
6930
|
+
continue;
|
|
6931
|
+
}
|
|
6932
|
+
|
|
6933
|
+
const wIdx = parseInt(wTok, 10);
|
|
6934
|
+
if (Number.isNaN(wIdx) || wIdx < 0 || wIdx >= (section.widgets || []).length) {
|
|
6935
|
+
warnings.push(`op[${i}]: widget index ${wIdx} out of range`);
|
|
6936
|
+
skipped++;
|
|
6937
|
+
continue;
|
|
6938
|
+
}
|
|
6939
|
+
const widget = section.widgets[wIdx];
|
|
6940
|
+
if (widget.locked) {
|
|
6941
|
+
warnings.push(`op[${i}]: widget "${widget.id}" is locked`);
|
|
6942
|
+
skipped++;
|
|
6943
|
+
continue;
|
|
6944
|
+
}
|
|
6945
|
+
|
|
6946
|
+
if (opType === "remove") {
|
|
6947
|
+
section.widgets.splice(wIdx, 1);
|
|
6948
|
+
applied++;
|
|
6949
|
+
} else if (opType === "replace" && op.value) {
|
|
6950
|
+
const wErr = validateWidget(op.value);
|
|
6951
|
+
if (wErr) { warnings.push(`op[${i}]: ${wErr}`); skipped++; continue; }
|
|
6952
|
+
section.widgets[wIdx] = { ...op.value, id: widget.id };
|
|
6953
|
+
applied++;
|
|
6954
|
+
} else if ((opType === "patch" || (opType === "replace" && isConfig)) && op.config) {
|
|
6955
|
+
section.widgets[wIdx] = { ...widget, config: { ...widget.config, ...op.config } };
|
|
6956
|
+
applied++;
|
|
6957
|
+
} else {
|
|
6958
|
+
warnings.push(`op[${i}]: widget-level op "${opType}" not handled`);
|
|
6959
|
+
skipped++;
|
|
6960
|
+
}
|
|
6961
|
+
}
|
|
6962
|
+
|
|
6963
|
+
const newLayout = { ...layout, sections };
|
|
6964
|
+
|
|
6965
|
+
const { error: saveErr } = await supabase
|
|
6966
|
+
.from("agent_office_layouts")
|
|
6967
|
+
.update({
|
|
6968
|
+
layout: newLayout,
|
|
6969
|
+
last_agent_update_at: new Date().toISOString(),
|
|
6970
|
+
updated_at: new Date().toISOString(),
|
|
6971
|
+
})
|
|
6972
|
+
.eq("id", office.id)
|
|
6973
|
+
.eq("user_id", userId);
|
|
6974
|
+
|
|
6975
|
+
if (saveErr) return err(`Office save failed: ${saveErr.message}`);
|
|
6976
|
+
|
|
6977
|
+
if (newSeedDataRows.length > 0) {
|
|
6978
|
+
const seedRows = newSeedDataRows.map((r) => ({
|
|
6979
|
+
user_id: userId,
|
|
6980
|
+
office_id: office.id,
|
|
6981
|
+
widget_id: r.widget_id,
|
|
6982
|
+
data: {},
|
|
6983
|
+
}));
|
|
6984
|
+
await supabase
|
|
6985
|
+
.from("agent_office_data")
|
|
6986
|
+
.upsert(seedRows, { onConflict: "office_id,widget_id" });
|
|
6987
|
+
}
|
|
6988
|
+
|
|
6989
|
+
await emitOfficeWebhook(api, "office_update", {
|
|
6990
|
+
user_id: userId,
|
|
6991
|
+
office_id: office.id,
|
|
6992
|
+
agent_id: agentId,
|
|
6993
|
+
});
|
|
6994
|
+
|
|
6995
|
+
return ok({ applied, skipped, warnings });
|
|
6996
|
+
}
|
|
6997
|
+
|
|
6998
|
+
case "update_widget_data": {
|
|
6999
|
+
if (!params.office_id) return err("Missing required: office_id");
|
|
7000
|
+
if (!params.widget_id) return err("Missing required: widget_id");
|
|
7001
|
+
if (typeof params.data !== "object" || params.data === null) return err("Missing required: data (object)");
|
|
7002
|
+
|
|
7003
|
+
const agentId = await resolveCallingAgent(params.agent_id as string);
|
|
7004
|
+
if (!agentId) return err("Could not resolve agent_id");
|
|
7005
|
+
|
|
7006
|
+
const { data: office, error: loadErr } = await supabase
|
|
7007
|
+
.from("agent_office_layouts")
|
|
7008
|
+
.select("id")
|
|
7009
|
+
.eq("id", params.office_id as string)
|
|
7010
|
+
.eq("user_id", userId)
|
|
7011
|
+
.eq("agent_id", agentId)
|
|
7012
|
+
.maybeSingle();
|
|
7013
|
+
|
|
7014
|
+
if (loadErr) return err(loadErr.message);
|
|
7015
|
+
if (!office) return err("Office not found or not owned by this agent");
|
|
7016
|
+
|
|
7017
|
+
const { error: upsertErr } = await supabase
|
|
7018
|
+
.from("agent_office_data")
|
|
7019
|
+
.upsert(
|
|
7020
|
+
{
|
|
7021
|
+
user_id: userId,
|
|
7022
|
+
office_id: office.id,
|
|
7023
|
+
widget_id: params.widget_id as string,
|
|
7024
|
+
data: params.data,
|
|
7025
|
+
updated_at: new Date().toISOString(),
|
|
7026
|
+
},
|
|
7027
|
+
{ onConflict: "office_id,widget_id" },
|
|
7028
|
+
);
|
|
7029
|
+
|
|
7030
|
+
if (upsertErr) return err(`Widget data save failed: ${upsertErr.message}`);
|
|
7031
|
+
|
|
7032
|
+
await emitOfficeWebhook(api, "office_data_update", {
|
|
7033
|
+
user_id: userId,
|
|
7034
|
+
office_id: office.id,
|
|
7035
|
+
agent_id: agentId,
|
|
7036
|
+
widget_id: params.widget_id,
|
|
7037
|
+
});
|
|
7038
|
+
|
|
7039
|
+
return ok({ message: `Widget ${params.widget_id} data updated` });
|
|
7040
|
+
}
|
|
7041
|
+
|
|
7042
|
+
default:
|
|
7043
|
+
return err(`Unknown action: ${action}. Use get_office, build_office, update_widgets, update_widget_data.`);
|
|
7044
|
+
}
|
|
7045
|
+
},
|
|
7046
|
+
}));
|
|
7047
|
+
}
|
|
7048
|
+
|
|
6528
7049
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
6529
7050
|
// Public: Register All Meta-Tools
|
|
6530
7051
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -6557,7 +7078,8 @@ export function registerTools(
|
|
|
6557
7078
|
registerSOPOps(api, supabase, userId, resolveAgent); // 13
|
|
6558
7079
|
registerBrainOps(api, supabase, userId, resolveAgent); // 14
|
|
6559
7080
|
registerTalentOps(api, supabase, userId); // 15
|
|
6560
|
-
registerFrameworkOps(api, supabase, userId, resolveAgent);
|
|
7081
|
+
registerFrameworkOps(api, supabase, userId, resolveAgent); // 16
|
|
7082
|
+
registerOfficeOps(api, supabase, userId, resolveAgent); // 17
|
|
6561
7083
|
|
|
6562
7084
|
// ── Register dynamic brain context hook ──
|
|
6563
7085
|
registerBrainContextHook(api, supabase, userId, fallbackAgentId);
|
|
@@ -6572,7 +7094,7 @@ export function registerTools(
|
|
|
6572
7094
|
registerBrainExtractionHook(api, supabase, userId, fallbackAgentId);
|
|
6573
7095
|
|
|
6574
7096
|
// ── Count and log ──
|
|
6575
|
-
const toolCount =
|
|
7097
|
+
const toolCount = 17;
|
|
6576
7098
|
const callerName = getCallingAgentName(api);
|
|
6577
7099
|
const agentLabel = fallbackAgentId || callerName || "auto-detect";
|
|
6578
7100
|
api.logger.info(`[ofiere] ${toolCount} meta-tools registered (agent: ${agentLabel})`);
|
|
@@ -6752,6 +7274,53 @@ function registerBrainExtractionHook(
|
|
|
6752
7274
|
} catch (logErr) {
|
|
6753
7275
|
api.logger.debug?.(`[ofiere-staff-report] dispatch_log update failed: ${logErr instanceof Error ? logErr.message : String(logErr)}`);
|
|
6754
7276
|
}
|
|
7277
|
+
|
|
7278
|
+
// Cycle 13 (BUG 2): flip the tasks row so the dashboard reflects
|
|
7279
|
+
// execution outcome. Without this, tasks stay at PENDING/IN_PROGRESS
|
|
7280
|
+
// forever and the proof-of-execution string lives only in
|
|
7281
|
+
// task_dispatch_log.response_preview (invisible to PM views).
|
|
7282
|
+
// Recurring tasks: stash output but leave status untouched so the
|
|
7283
|
+
// next cron tick can re-fire. One-shot: mark DONE, set progress=100,
|
|
7284
|
+
// stamp completed_at, persist proof in custom_fields.last_output.
|
|
7285
|
+
try {
|
|
7286
|
+
const { data: schedRow } = await supabase
|
|
7287
|
+
.from("scheduler_events")
|
|
7288
|
+
.select("recurrence_type")
|
|
7289
|
+
.eq("task_id", taskId)
|
|
7290
|
+
.maybeSingle();
|
|
7291
|
+
const isRecurring = !!(schedRow?.recurrence_type && schedRow.recurrence_type !== "none");
|
|
7292
|
+
|
|
7293
|
+
const { data: curTask } = await supabase
|
|
7294
|
+
.from("tasks")
|
|
7295
|
+
.select("custom_fields, status")
|
|
7296
|
+
.eq("id", taskId)
|
|
7297
|
+
.eq("user_id", userId)
|
|
7298
|
+
.maybeSingle();
|
|
7299
|
+
const cf = ((curTask?.custom_fields as Record<string, any>) ?? {}) as Record<string, any>;
|
|
7300
|
+
cf.last_output = excerpt;
|
|
7301
|
+
cf.last_completed_session = sessionKey ?? null;
|
|
7302
|
+
cf.last_run_at = new Date().toISOString();
|
|
7303
|
+
|
|
7304
|
+
if (isRecurring) {
|
|
7305
|
+
await supabase.from("tasks")
|
|
7306
|
+
.update({ custom_fields: cf, updated_at: new Date().toISOString() })
|
|
7307
|
+
.eq("id", taskId)
|
|
7308
|
+
.eq("user_id", userId);
|
|
7309
|
+
} else if (curTask?.status !== "FAILED") {
|
|
7310
|
+
await supabase.from("tasks")
|
|
7311
|
+
.update({
|
|
7312
|
+
status: "DONE",
|
|
7313
|
+
completed_at: new Date().toISOString(),
|
|
7314
|
+
progress: 100,
|
|
7315
|
+
custom_fields: cf,
|
|
7316
|
+
updated_at: new Date().toISOString(),
|
|
7317
|
+
})
|
|
7318
|
+
.eq("id", taskId)
|
|
7319
|
+
.eq("user_id", userId);
|
|
7320
|
+
}
|
|
7321
|
+
} catch (tErr) {
|
|
7322
|
+
api.logger.debug?.(`[ofiere-staff-report] tasks update failed: ${tErr instanceof Error ? tErr.message : String(tErr)}`);
|
|
7323
|
+
}
|
|
6755
7324
|
}
|
|
6756
7325
|
|
|
6757
7326
|
// BUG 6 fix (BUGSHOOT-2): mirror the assistant turn into the
|
|
@@ -6834,6 +7403,51 @@ function registerBrainExtractionHook(
|
|
|
6834
7403
|
} catch (logErr) {
|
|
6835
7404
|
api.logger.debug?.(`[ofiere-brain] dispatch_log update failed: ${logErr instanceof Error ? logErr.message : String(logErr)}`);
|
|
6836
7405
|
}
|
|
7406
|
+
|
|
7407
|
+
// Cycle 13 (BUG 2): flip the tasks row for chief-direct (non-staff)
|
|
7408
|
+
// dispatches too — same logic as the staff path above. Recurring
|
|
7409
|
+
// tasks keep their status; one-shots flip to DONE with the proof
|
|
7410
|
+
// string stored in custom_fields.last_output.
|
|
7411
|
+
try {
|
|
7412
|
+
const excerpt = lastAssistant.slice(0, 1500);
|
|
7413
|
+
const { data: schedRow } = await supabase
|
|
7414
|
+
.from("scheduler_events")
|
|
7415
|
+
.select("recurrence_type")
|
|
7416
|
+
.eq("task_id", dispatchTaskIdFromCtx)
|
|
7417
|
+
.maybeSingle();
|
|
7418
|
+
const isRecurring = !!(schedRow?.recurrence_type && schedRow.recurrence_type !== "none");
|
|
7419
|
+
|
|
7420
|
+
const { data: curTask } = await supabase
|
|
7421
|
+
.from("tasks")
|
|
7422
|
+
.select("custom_fields, status")
|
|
7423
|
+
.eq("id", dispatchTaskIdFromCtx)
|
|
7424
|
+
.eq("user_id", userId)
|
|
7425
|
+
.maybeSingle();
|
|
7426
|
+
const cf = ((curTask?.custom_fields as Record<string, any>) ?? {}) as Record<string, any>;
|
|
7427
|
+
cf.last_output = excerpt;
|
|
7428
|
+
cf.last_completed_session = sessionKey ?? null;
|
|
7429
|
+
cf.last_run_at = new Date().toISOString();
|
|
7430
|
+
|
|
7431
|
+
if (isRecurring) {
|
|
7432
|
+
await supabase.from("tasks")
|
|
7433
|
+
.update({ custom_fields: cf, updated_at: new Date().toISOString() })
|
|
7434
|
+
.eq("id", dispatchTaskIdFromCtx)
|
|
7435
|
+
.eq("user_id", userId);
|
|
7436
|
+
} else if (curTask?.status !== "FAILED") {
|
|
7437
|
+
await supabase.from("tasks")
|
|
7438
|
+
.update({
|
|
7439
|
+
status: "DONE",
|
|
7440
|
+
completed_at: new Date().toISOString(),
|
|
7441
|
+
progress: 100,
|
|
7442
|
+
custom_fields: cf,
|
|
7443
|
+
updated_at: new Date().toISOString(),
|
|
7444
|
+
})
|
|
7445
|
+
.eq("id", dispatchTaskIdFromCtx)
|
|
7446
|
+
.eq("user_id", userId);
|
|
7447
|
+
}
|
|
7448
|
+
} catch (tErr) {
|
|
7449
|
+
api.logger.debug?.(`[ofiere-brain] tasks update failed: ${tErr instanceof Error ? tErr.message : String(tErr)}`);
|
|
7450
|
+
}
|
|
6837
7451
|
}
|
|
6838
7452
|
|
|
6839
7453
|
// Cycle 7b BUGSHOOT-4 — trivial-skip moved here from line ~6593.
|