ofiere-openclaw-plugin 4.53.0 → 4.54.1
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 +432 -1
- package/openclaw.plugin.json +2 -1
- package/package.json +2 -2
- package/src/tools.ts +450 -2
package/dist/src/tools.js
CHANGED
|
@@ -6462,6 +6462,436 @@ function registerTalentContextHook(api, supabase, userId, fallbackAgentId) {
|
|
|
6462
6462
|
}
|
|
6463
6463
|
}
|
|
6464
6464
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
6465
|
+
// META-TOOL 17: OFIERE_OFFICE_OPS — Agent Office Canvas (widget dashboard)
|
|
6466
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
6467
|
+
async function emitOfficeWebhook(api, type, payload) {
|
|
6468
|
+
const webhookUrl = process.env.OFIERE_DASHBOARD_WEBHOOK_URL || "";
|
|
6469
|
+
const webhookSecret = process.env.OPENCLAW_WEBHOOK_SECRET || "";
|
|
6470
|
+
if (!webhookUrl || !webhookSecret)
|
|
6471
|
+
return;
|
|
6472
|
+
try {
|
|
6473
|
+
await fetch(webhookUrl, {
|
|
6474
|
+
method: "POST",
|
|
6475
|
+
headers: {
|
|
6476
|
+
"content-type": "application/json",
|
|
6477
|
+
authorization: `Bearer ${webhookSecret}`,
|
|
6478
|
+
},
|
|
6479
|
+
body: JSON.stringify({ type, payload }),
|
|
6480
|
+
});
|
|
6481
|
+
}
|
|
6482
|
+
catch (wErr) {
|
|
6483
|
+
api.logger.debug?.(`[ofiere-office] webhook post failed: ${wErr instanceof Error ? wErr.message : String(wErr)}`);
|
|
6484
|
+
}
|
|
6485
|
+
}
|
|
6486
|
+
function validateSection(s) {
|
|
6487
|
+
if (!s || typeof s !== "object")
|
|
6488
|
+
return "section is not an object";
|
|
6489
|
+
if (typeof s.title !== "string" || !s.title)
|
|
6490
|
+
return "section.title required";
|
|
6491
|
+
if (typeof s.order !== "number")
|
|
6492
|
+
return "section.order must be a number";
|
|
6493
|
+
if (!Array.isArray(s.widgets))
|
|
6494
|
+
return "section.widgets must be an array";
|
|
6495
|
+
for (const w of s.widgets) {
|
|
6496
|
+
const wErr = validateWidget(w);
|
|
6497
|
+
if (wErr)
|
|
6498
|
+
return `section "${s.title}": ${wErr}`;
|
|
6499
|
+
}
|
|
6500
|
+
return null;
|
|
6501
|
+
}
|
|
6502
|
+
function validateWidget(w) {
|
|
6503
|
+
if (!w || typeof w !== "object")
|
|
6504
|
+
return "widget is not an object";
|
|
6505
|
+
if (typeof w.type !== "string" || !w.type)
|
|
6506
|
+
return "widget.type required";
|
|
6507
|
+
if (typeof w.config !== "object" || w.config === null)
|
|
6508
|
+
return "widget.config must be an object";
|
|
6509
|
+
const ly = w.layout;
|
|
6510
|
+
if (!ly || typeof ly !== "object")
|
|
6511
|
+
return "widget.layout required";
|
|
6512
|
+
if (typeof ly.x !== "number" || typeof ly.y !== "number" || typeof ly.w !== "number" || typeof ly.h !== "number") {
|
|
6513
|
+
return "widget.layout must have numeric x, y, w, h";
|
|
6514
|
+
}
|
|
6515
|
+
return null;
|
|
6516
|
+
}
|
|
6517
|
+
function shortId(prefix) {
|
|
6518
|
+
return `${prefix}_${Math.random().toString(36).slice(2, 10)}`;
|
|
6519
|
+
}
|
|
6520
|
+
function registerOfficeOps(api, supabase, userId, resolveAgent) {
|
|
6521
|
+
api.registerTool((toolCtx) => ({
|
|
6522
|
+
name: "OFIERE_OFFICE_OPS",
|
|
6523
|
+
label: "Ofiere Office Operations",
|
|
6524
|
+
description: `Build and update your department Office — a widget-based dashboard canvas bound to a PM Space.\n\n` +
|
|
6525
|
+
`Actions:\n` +
|
|
6526
|
+
`- "get_office": Load the current layout + widget data for a given (space_id, agent_id). Optional: space_id, agent_id.\n` +
|
|
6527
|
+
`- "build_office": One-time initial build. Required: space_id, sections. Only callable while the office row is in 'initializing' state.\n` +
|
|
6528
|
+
`- "update_widgets": Modify the layout (add/remove/replace/patch widgets or sections). Required: office_id, operations. Locked items are skipped (returns warnings).\n` +
|
|
6529
|
+
`- "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` +
|
|
6530
|
+
`Layout shape:\n` +
|
|
6531
|
+
` { version: 1, sections: [ { id, title, description?, order, locked?, collapsed?, columns, widgets: [ { id, type, config, layout: {x,y,w,h}, locked?, data_source? } ] } ] }\n\n` +
|
|
6532
|
+
`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` +
|
|
6533
|
+
`Operation shape (RFC-6902 flavored): { op: 'add'|'remove'|'replace'|'patch', path: '/sections/0/widgets/-' or '/sections/0/widgets/0', value?, widget?, config? }`,
|
|
6534
|
+
parameters: {
|
|
6535
|
+
type: "object",
|
|
6536
|
+
required: ["action"],
|
|
6537
|
+
properties: {
|
|
6538
|
+
action: {
|
|
6539
|
+
type: "string",
|
|
6540
|
+
description: "get_office, build_office, update_widgets, update_widget_data",
|
|
6541
|
+
},
|
|
6542
|
+
space_id: { type: "string", description: "PM space UUID" },
|
|
6543
|
+
agent_id: { type: "string", description: "Agent identifier (defaults to calling agent)" },
|
|
6544
|
+
office_id: { type: "string", description: "Office row UUID" },
|
|
6545
|
+
widget_id: { type: "string", description: "Widget short id" },
|
|
6546
|
+
sections: { type: "array", description: "Initial layout sections (build_office only)" },
|
|
6547
|
+
operations: { type: "array", description: "Patch operations (update_widgets only)" },
|
|
6548
|
+
data: { type: "object", description: "Widget data payload (update_widget_data only)" },
|
|
6549
|
+
},
|
|
6550
|
+
},
|
|
6551
|
+
async execute(_id, params) {
|
|
6552
|
+
const action = params.action;
|
|
6553
|
+
const ctxAgentHint = toolCtx?.agentAccountId || toolCtx?.agentId || "";
|
|
6554
|
+
async function resolveCallingAgent(explicitId) {
|
|
6555
|
+
if (explicitId && explicitId.trim())
|
|
6556
|
+
return resolveAgent(explicitId);
|
|
6557
|
+
if (ctxAgentHint)
|
|
6558
|
+
return resolveAgent(ctxAgentHint);
|
|
6559
|
+
return resolveAgent();
|
|
6560
|
+
}
|
|
6561
|
+
switch (action) {
|
|
6562
|
+
case "get_office": {
|
|
6563
|
+
const agentId = await resolveCallingAgent(params.agent_id);
|
|
6564
|
+
if (!agentId)
|
|
6565
|
+
return err("Could not resolve agent_id");
|
|
6566
|
+
let q = supabase
|
|
6567
|
+
.from("agent_office_layouts")
|
|
6568
|
+
.select("id, space_id, agent_id, status, is_finalized, layout, last_agent_update_at, error_message, updated_at")
|
|
6569
|
+
.eq("user_id", userId)
|
|
6570
|
+
.eq("agent_id", agentId);
|
|
6571
|
+
if (params.space_id)
|
|
6572
|
+
q = q.eq("space_id", params.space_id);
|
|
6573
|
+
const { data: rows, error } = await q.limit(1);
|
|
6574
|
+
if (error)
|
|
6575
|
+
return err(error.message);
|
|
6576
|
+
if (!rows || rows.length === 0)
|
|
6577
|
+
return err("No office found for this agent");
|
|
6578
|
+
const office = rows[0];
|
|
6579
|
+
const { data: widgetData } = await supabase
|
|
6580
|
+
.from("agent_office_data")
|
|
6581
|
+
.select("widget_id, data, updated_at")
|
|
6582
|
+
.eq("user_id", userId)
|
|
6583
|
+
.eq("office_id", office.id);
|
|
6584
|
+
const dataMap = {};
|
|
6585
|
+
for (const row of widgetData || []) {
|
|
6586
|
+
dataMap[row.widget_id] = row.data;
|
|
6587
|
+
}
|
|
6588
|
+
return ok({ office, widget_data: dataMap });
|
|
6589
|
+
}
|
|
6590
|
+
case "build_office": {
|
|
6591
|
+
if (!params.space_id)
|
|
6592
|
+
return err("Missing required: space_id");
|
|
6593
|
+
if (!Array.isArray(params.sections))
|
|
6594
|
+
return err("Missing required: sections (array)");
|
|
6595
|
+
const agentId = await resolveCallingAgent(params.agent_id);
|
|
6596
|
+
if (!agentId)
|
|
6597
|
+
return err("Could not resolve agent_id");
|
|
6598
|
+
const inputSections = params.sections;
|
|
6599
|
+
for (let i = 0; i < inputSections.length; i++) {
|
|
6600
|
+
const sErr = validateSection(inputSections[i]);
|
|
6601
|
+
if (sErr)
|
|
6602
|
+
return err(`Validation failed at section[${i}]: ${sErr}`);
|
|
6603
|
+
}
|
|
6604
|
+
const { data: existing, error: loadErr } = await supabase
|
|
6605
|
+
.from("agent_office_layouts")
|
|
6606
|
+
.select("id, status")
|
|
6607
|
+
.eq("user_id", userId)
|
|
6608
|
+
.eq("space_id", params.space_id)
|
|
6609
|
+
.eq("agent_id", agentId)
|
|
6610
|
+
.maybeSingle();
|
|
6611
|
+
if (loadErr)
|
|
6612
|
+
return err(`Office lookup failed: ${loadErr.message}`);
|
|
6613
|
+
if (!existing)
|
|
6614
|
+
return err("Office row not found — dashboard must call /api/office/initialize first");
|
|
6615
|
+
if (existing.status !== "initializing") {
|
|
6616
|
+
return err(`Office already built (status=${existing.status}). Use update_widgets to modify.`);
|
|
6617
|
+
}
|
|
6618
|
+
const normalized = inputSections.map((s, idx) => {
|
|
6619
|
+
const widgets = (s.widgets || []).map((w) => ({
|
|
6620
|
+
id: typeof w.id === "string" && w.id ? w.id : shortId("wgt"),
|
|
6621
|
+
type: w.type,
|
|
6622
|
+
config: w.config || {},
|
|
6623
|
+
layout: {
|
|
6624
|
+
x: w.layout.x,
|
|
6625
|
+
y: w.layout.y,
|
|
6626
|
+
w: w.layout.w,
|
|
6627
|
+
h: w.layout.h,
|
|
6628
|
+
minW: w.layout.minW,
|
|
6629
|
+
minH: w.layout.minH,
|
|
6630
|
+
maxW: w.layout.maxW,
|
|
6631
|
+
maxH: w.layout.maxH,
|
|
6632
|
+
},
|
|
6633
|
+
locked: w.locked === true,
|
|
6634
|
+
data_source: w.data_source || { type: "agent", refresh_interval: 3600, last_refreshed_at: null },
|
|
6635
|
+
}));
|
|
6636
|
+
return {
|
|
6637
|
+
id: typeof s.id === "string" && s.id ? s.id : shortId("sec"),
|
|
6638
|
+
title: s.title,
|
|
6639
|
+
description: s.description,
|
|
6640
|
+
order: typeof s.order === "number" ? s.order : idx,
|
|
6641
|
+
locked: s.locked === true,
|
|
6642
|
+
collapsed: s.collapsed === true,
|
|
6643
|
+
columns: typeof s.columns === "number" ? s.columns : 3,
|
|
6644
|
+
widgets,
|
|
6645
|
+
};
|
|
6646
|
+
});
|
|
6647
|
+
const layout = { version: 1, sections: normalized };
|
|
6648
|
+
const { error: updateErr } = await supabase
|
|
6649
|
+
.from("agent_office_layouts")
|
|
6650
|
+
.update({
|
|
6651
|
+
layout,
|
|
6652
|
+
status: "active",
|
|
6653
|
+
last_agent_update_at: new Date().toISOString(),
|
|
6654
|
+
updated_at: new Date().toISOString(),
|
|
6655
|
+
})
|
|
6656
|
+
.eq("id", existing.id)
|
|
6657
|
+
.eq("user_id", userId);
|
|
6658
|
+
if (updateErr)
|
|
6659
|
+
return err(`Office save failed: ${updateErr.message}`);
|
|
6660
|
+
const dataRows = normalized.flatMap((s) => s.widgets.map((w) => ({
|
|
6661
|
+
user_id: userId,
|
|
6662
|
+
office_id: existing.id,
|
|
6663
|
+
widget_id: w.id,
|
|
6664
|
+
data: {},
|
|
6665
|
+
})));
|
|
6666
|
+
if (dataRows.length > 0) {
|
|
6667
|
+
const { error: dataErr } = await supabase
|
|
6668
|
+
.from("agent_office_data")
|
|
6669
|
+
.upsert(dataRows, { onConflict: "office_id,widget_id" });
|
|
6670
|
+
if (dataErr) {
|
|
6671
|
+
api.logger.warn?.(`[ofiere-office] widget_data seed failed: ${dataErr.message}`);
|
|
6672
|
+
}
|
|
6673
|
+
}
|
|
6674
|
+
await emitOfficeWebhook(api, "office_update", {
|
|
6675
|
+
user_id: userId,
|
|
6676
|
+
office_id: existing.id,
|
|
6677
|
+
agent_id: agentId,
|
|
6678
|
+
});
|
|
6679
|
+
return ok({
|
|
6680
|
+
message: `Office built with ${normalized.length} sections and ${dataRows.length} widgets`,
|
|
6681
|
+
office_id: existing.id,
|
|
6682
|
+
section_count: normalized.length,
|
|
6683
|
+
widget_count: dataRows.length,
|
|
6684
|
+
});
|
|
6685
|
+
}
|
|
6686
|
+
case "update_widgets": {
|
|
6687
|
+
if (!params.office_id)
|
|
6688
|
+
return err("Missing required: office_id");
|
|
6689
|
+
if (!Array.isArray(params.operations))
|
|
6690
|
+
return err("Missing required: operations (array)");
|
|
6691
|
+
const operations = params.operations;
|
|
6692
|
+
const agentId = await resolveCallingAgent(params.agent_id);
|
|
6693
|
+
if (!agentId)
|
|
6694
|
+
return err("Could not resolve agent_id");
|
|
6695
|
+
const { data: office, error: loadErr } = await supabase
|
|
6696
|
+
.from("agent_office_layouts")
|
|
6697
|
+
.select("id, layout, is_finalized, agent_id")
|
|
6698
|
+
.eq("id", params.office_id)
|
|
6699
|
+
.eq("user_id", userId)
|
|
6700
|
+
.eq("agent_id", agentId)
|
|
6701
|
+
.maybeSingle();
|
|
6702
|
+
if (loadErr)
|
|
6703
|
+
return err(loadErr.message);
|
|
6704
|
+
if (!office)
|
|
6705
|
+
return err("Office not found or not owned by this agent");
|
|
6706
|
+
const layout = office.layout || { version: 1, sections: [] };
|
|
6707
|
+
const sections = Array.isArray(layout.sections) ? [...layout.sections] : [];
|
|
6708
|
+
const warnings = [];
|
|
6709
|
+
const newSeedDataRows = [];
|
|
6710
|
+
let applied = 0;
|
|
6711
|
+
let skipped = 0;
|
|
6712
|
+
for (let i = 0; i < operations.length; i++) {
|
|
6713
|
+
const op = operations[i];
|
|
6714
|
+
const path = op?.path || "";
|
|
6715
|
+
const opType = op?.op || "";
|
|
6716
|
+
const m = path.match(/^\/sections\/(\d+)(?:\/widgets\/(-|\d+))?(?:\/config)?$/);
|
|
6717
|
+
if (!m) {
|
|
6718
|
+
warnings.push(`op[${i}]: unsupported path "${path}"`);
|
|
6719
|
+
skipped++;
|
|
6720
|
+
continue;
|
|
6721
|
+
}
|
|
6722
|
+
const sIdx = parseInt(m[1], 10);
|
|
6723
|
+
const wTok = m[2];
|
|
6724
|
+
const isConfig = path.endsWith("/config");
|
|
6725
|
+
if (Number.isNaN(sIdx) || sIdx < 0 || sIdx >= sections.length) {
|
|
6726
|
+
warnings.push(`op[${i}]: section index ${sIdx} out of range`);
|
|
6727
|
+
skipped++;
|
|
6728
|
+
continue;
|
|
6729
|
+
}
|
|
6730
|
+
const section = sections[sIdx];
|
|
6731
|
+
if (section.locked) {
|
|
6732
|
+
warnings.push(`op[${i}]: section "${section.title}" is locked`);
|
|
6733
|
+
skipped++;
|
|
6734
|
+
continue;
|
|
6735
|
+
}
|
|
6736
|
+
if (wTok === undefined) {
|
|
6737
|
+
if (opType === "remove") {
|
|
6738
|
+
sections.splice(sIdx, 1);
|
|
6739
|
+
applied++;
|
|
6740
|
+
continue;
|
|
6741
|
+
}
|
|
6742
|
+
if (opType === "replace" && op.value) {
|
|
6743
|
+
const sErr = validateSection(op.value);
|
|
6744
|
+
if (sErr) {
|
|
6745
|
+
warnings.push(`op[${i}]: ${sErr}`);
|
|
6746
|
+
skipped++;
|
|
6747
|
+
continue;
|
|
6748
|
+
}
|
|
6749
|
+
sections[sIdx] = { ...op.value, id: section.id };
|
|
6750
|
+
applied++;
|
|
6751
|
+
continue;
|
|
6752
|
+
}
|
|
6753
|
+
warnings.push(`op[${i}]: section-level op "${opType}" not supported (use replace/remove)`);
|
|
6754
|
+
skipped++;
|
|
6755
|
+
continue;
|
|
6756
|
+
}
|
|
6757
|
+
if (wTok === "-") {
|
|
6758
|
+
if (opType !== "add") {
|
|
6759
|
+
warnings.push(`op[${i}]: path "/widgets/-" requires op=add`);
|
|
6760
|
+
skipped++;
|
|
6761
|
+
continue;
|
|
6762
|
+
}
|
|
6763
|
+
const w = op.widget || op.value;
|
|
6764
|
+
const wErr = validateWidget(w);
|
|
6765
|
+
if (wErr) {
|
|
6766
|
+
warnings.push(`op[${i}]: ${wErr}`);
|
|
6767
|
+
skipped++;
|
|
6768
|
+
continue;
|
|
6769
|
+
}
|
|
6770
|
+
const newW = {
|
|
6771
|
+
id: typeof w.id === "string" && w.id ? w.id : shortId("wgt"),
|
|
6772
|
+
type: w.type,
|
|
6773
|
+
config: w.config || {},
|
|
6774
|
+
layout: { ...w.layout },
|
|
6775
|
+
locked: w.locked === true,
|
|
6776
|
+
data_source: w.data_source || { type: "agent", refresh_interval: 3600, last_refreshed_at: null },
|
|
6777
|
+
};
|
|
6778
|
+
section.widgets = [...(section.widgets || []), newW];
|
|
6779
|
+
newSeedDataRows.push({ widget_id: newW.id });
|
|
6780
|
+
applied++;
|
|
6781
|
+
continue;
|
|
6782
|
+
}
|
|
6783
|
+
const wIdx = parseInt(wTok, 10);
|
|
6784
|
+
if (Number.isNaN(wIdx) || wIdx < 0 || wIdx >= (section.widgets || []).length) {
|
|
6785
|
+
warnings.push(`op[${i}]: widget index ${wIdx} out of range`);
|
|
6786
|
+
skipped++;
|
|
6787
|
+
continue;
|
|
6788
|
+
}
|
|
6789
|
+
const widget = section.widgets[wIdx];
|
|
6790
|
+
if (widget.locked) {
|
|
6791
|
+
warnings.push(`op[${i}]: widget "${widget.id}" is locked`);
|
|
6792
|
+
skipped++;
|
|
6793
|
+
continue;
|
|
6794
|
+
}
|
|
6795
|
+
if (opType === "remove") {
|
|
6796
|
+
section.widgets.splice(wIdx, 1);
|
|
6797
|
+
applied++;
|
|
6798
|
+
}
|
|
6799
|
+
else if (opType === "replace" && op.value) {
|
|
6800
|
+
const wErr = validateWidget(op.value);
|
|
6801
|
+
if (wErr) {
|
|
6802
|
+
warnings.push(`op[${i}]: ${wErr}`);
|
|
6803
|
+
skipped++;
|
|
6804
|
+
continue;
|
|
6805
|
+
}
|
|
6806
|
+
section.widgets[wIdx] = { ...op.value, id: widget.id };
|
|
6807
|
+
applied++;
|
|
6808
|
+
}
|
|
6809
|
+
else if ((opType === "patch" || (opType === "replace" && isConfig)) && op.config) {
|
|
6810
|
+
section.widgets[wIdx] = { ...widget, config: { ...widget.config, ...op.config } };
|
|
6811
|
+
applied++;
|
|
6812
|
+
}
|
|
6813
|
+
else {
|
|
6814
|
+
warnings.push(`op[${i}]: widget-level op "${opType}" not handled`);
|
|
6815
|
+
skipped++;
|
|
6816
|
+
}
|
|
6817
|
+
}
|
|
6818
|
+
const newLayout = { ...layout, sections };
|
|
6819
|
+
const { error: saveErr } = await supabase
|
|
6820
|
+
.from("agent_office_layouts")
|
|
6821
|
+
.update({
|
|
6822
|
+
layout: newLayout,
|
|
6823
|
+
last_agent_update_at: new Date().toISOString(),
|
|
6824
|
+
updated_at: new Date().toISOString(),
|
|
6825
|
+
})
|
|
6826
|
+
.eq("id", office.id)
|
|
6827
|
+
.eq("user_id", userId);
|
|
6828
|
+
if (saveErr)
|
|
6829
|
+
return err(`Office save failed: ${saveErr.message}`);
|
|
6830
|
+
if (newSeedDataRows.length > 0) {
|
|
6831
|
+
const seedRows = newSeedDataRows.map((r) => ({
|
|
6832
|
+
user_id: userId,
|
|
6833
|
+
office_id: office.id,
|
|
6834
|
+
widget_id: r.widget_id,
|
|
6835
|
+
data: {},
|
|
6836
|
+
}));
|
|
6837
|
+
await supabase
|
|
6838
|
+
.from("agent_office_data")
|
|
6839
|
+
.upsert(seedRows, { onConflict: "office_id,widget_id" });
|
|
6840
|
+
}
|
|
6841
|
+
await emitOfficeWebhook(api, "office_update", {
|
|
6842
|
+
user_id: userId,
|
|
6843
|
+
office_id: office.id,
|
|
6844
|
+
agent_id: agentId,
|
|
6845
|
+
});
|
|
6846
|
+
return ok({ applied, skipped, warnings });
|
|
6847
|
+
}
|
|
6848
|
+
case "update_widget_data": {
|
|
6849
|
+
if (!params.office_id)
|
|
6850
|
+
return err("Missing required: office_id");
|
|
6851
|
+
if (!params.widget_id)
|
|
6852
|
+
return err("Missing required: widget_id");
|
|
6853
|
+
if (typeof params.data !== "object" || params.data === null)
|
|
6854
|
+
return err("Missing required: data (object)");
|
|
6855
|
+
const agentId = await resolveCallingAgent(params.agent_id);
|
|
6856
|
+
if (!agentId)
|
|
6857
|
+
return err("Could not resolve agent_id");
|
|
6858
|
+
const { data: office, error: loadErr } = await supabase
|
|
6859
|
+
.from("agent_office_layouts")
|
|
6860
|
+
.select("id")
|
|
6861
|
+
.eq("id", params.office_id)
|
|
6862
|
+
.eq("user_id", userId)
|
|
6863
|
+
.eq("agent_id", agentId)
|
|
6864
|
+
.maybeSingle();
|
|
6865
|
+
if (loadErr)
|
|
6866
|
+
return err(loadErr.message);
|
|
6867
|
+
if (!office)
|
|
6868
|
+
return err("Office not found or not owned by this agent");
|
|
6869
|
+
const { error: upsertErr } = await supabase
|
|
6870
|
+
.from("agent_office_data")
|
|
6871
|
+
.upsert({
|
|
6872
|
+
user_id: userId,
|
|
6873
|
+
office_id: office.id,
|
|
6874
|
+
widget_id: params.widget_id,
|
|
6875
|
+
data: params.data,
|
|
6876
|
+
updated_at: new Date().toISOString(),
|
|
6877
|
+
}, { onConflict: "office_id,widget_id" });
|
|
6878
|
+
if (upsertErr)
|
|
6879
|
+
return err(`Widget data save failed: ${upsertErr.message}`);
|
|
6880
|
+
await emitOfficeWebhook(api, "office_data_update", {
|
|
6881
|
+
user_id: userId,
|
|
6882
|
+
office_id: office.id,
|
|
6883
|
+
agent_id: agentId,
|
|
6884
|
+
widget_id: params.widget_id,
|
|
6885
|
+
});
|
|
6886
|
+
return ok({ message: `Widget ${params.widget_id} data updated` });
|
|
6887
|
+
}
|
|
6888
|
+
default:
|
|
6889
|
+
return err(`Unknown action: ${action}. Use get_office, build_office, update_widgets, update_widget_data.`);
|
|
6890
|
+
}
|
|
6891
|
+
},
|
|
6892
|
+
}));
|
|
6893
|
+
}
|
|
6894
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
6465
6895
|
// Public: Register All Meta-Tools
|
|
6466
6896
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
6467
6897
|
// This is the single entry point called by index.ts.
|
|
@@ -6488,6 +6918,7 @@ supabase, config) {
|
|
|
6488
6918
|
registerBrainOps(api, supabase, userId, resolveAgent); // 14
|
|
6489
6919
|
registerTalentOps(api, supabase, userId); // 15
|
|
6490
6920
|
registerFrameworkOps(api, supabase, userId, resolveAgent); // 16
|
|
6921
|
+
registerOfficeOps(api, supabase, userId, resolveAgent); // 17
|
|
6491
6922
|
// ── Register dynamic brain context hook ──
|
|
6492
6923
|
registerBrainContextHook(api, supabase, userId, fallbackAgentId);
|
|
6493
6924
|
// ── Register talent context hook ──
|
|
@@ -6497,7 +6928,7 @@ supabase, config) {
|
|
|
6497
6928
|
// ── Register agent_end hook for server-side brain extraction ──
|
|
6498
6929
|
registerBrainExtractionHook(api, supabase, userId, fallbackAgentId);
|
|
6499
6930
|
// ── Count and log ──
|
|
6500
|
-
const toolCount =
|
|
6931
|
+
const toolCount = 17;
|
|
6501
6932
|
const callerName = getCallingAgentName(api);
|
|
6502
6933
|
const agentLabel = fallbackAgentId || callerName || "auto-detect";
|
|
6503
6934
|
api.logger.info(`[ofiere] ${toolCount} meta-tools registered (agent: ${agentLabel})`);
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ofiere-openclaw-plugin",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.54.1",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "OpenClaw plugin for Ofiere PM -
|
|
5
|
+
"description": "OpenClaw plugin for Ofiere PM - 17 meta-tools covering tasks, agents, projects, scheduling, knowledge, workflows, notifications, memory, prompts, constellation, space file management, execution plan builder, SOP management, agent brain, talent management, corporate frameworks, and agent office canvas",
|
|
6
6
|
"keywords": ["openclaw", "ofiere", "project-management", "agents", "plugin"],
|
|
7
7
|
"homepage": "https://github.com/gilanggemar/Ofiere",
|
|
8
8
|
"repository": {
|
package/src/tools.ts
CHANGED
|
@@ -6599,6 +6599,453 @@ function registerTalentContextHook(
|
|
|
6599
6599
|
}
|
|
6600
6600
|
}
|
|
6601
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
|
+
|
|
6602
7049
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
6603
7050
|
// Public: Register All Meta-Tools
|
|
6604
7051
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -6631,7 +7078,8 @@ export function registerTools(
|
|
|
6631
7078
|
registerSOPOps(api, supabase, userId, resolveAgent); // 13
|
|
6632
7079
|
registerBrainOps(api, supabase, userId, resolveAgent); // 14
|
|
6633
7080
|
registerTalentOps(api, supabase, userId); // 15
|
|
6634
|
-
registerFrameworkOps(api, supabase, userId, resolveAgent);
|
|
7081
|
+
registerFrameworkOps(api, supabase, userId, resolveAgent); // 16
|
|
7082
|
+
registerOfficeOps(api, supabase, userId, resolveAgent); // 17
|
|
6635
7083
|
|
|
6636
7084
|
// ── Register dynamic brain context hook ──
|
|
6637
7085
|
registerBrainContextHook(api, supabase, userId, fallbackAgentId);
|
|
@@ -6646,7 +7094,7 @@ export function registerTools(
|
|
|
6646
7094
|
registerBrainExtractionHook(api, supabase, userId, fallbackAgentId);
|
|
6647
7095
|
|
|
6648
7096
|
// ── Count and log ──
|
|
6649
|
-
const toolCount =
|
|
7097
|
+
const toolCount = 17;
|
|
6650
7098
|
const callerName = getCallingAgentName(api);
|
|
6651
7099
|
const agentLabel = fallbackAgentId || callerName || "auto-detect";
|
|
6652
7100
|
api.logger.info(`[ofiere] ${toolCount} meta-tools registered (agent: ${agentLabel})`);
|