@useorgx/openclaw-plugin 0.4.5 → 0.4.6
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 +24 -3
- package/dashboard/dist/assets/0tOC3wSN.js +214 -0
- package/dashboard/dist/assets/B3ziCA02.js +8 -0
- package/dashboard/dist/assets/Bm8QnMJ_.js +1 -0
- package/dashboard/dist/assets/CpJsfbXo.js +9 -0
- package/dashboard/dist/assets/CyxZio4Y.js +1 -0
- package/dashboard/dist/assets/DaAIOik3.css +1 -0
- package/dashboard/dist/assets/sAhvFnpk.js +4 -0
- package/dashboard/dist/index.html +5 -5
- package/dist/activity-store.d.ts +28 -0
- package/dist/activity-store.js +250 -0
- package/dist/agent-context-store.d.ts +19 -0
- package/dist/agent-context-store.js +60 -3
- package/dist/agent-suite.d.ts +83 -0
- package/dist/agent-suite.js +615 -0
- package/dist/contracts/client.d.ts +22 -1
- package/dist/contracts/client.js +120 -3
- package/dist/contracts/types.d.ts +190 -1
- package/dist/entity-comment-store.d.ts +29 -0
- package/dist/entity-comment-store.js +190 -0
- package/dist/hooks/post-reporting-event.mjs +326 -0
- package/dist/http-handler.d.ts +7 -1
- package/dist/http-handler.js +3603 -578
- package/dist/index.js +936 -62
- package/dist/mcp-client-setup.js +156 -24
- package/dist/mcp-http-handler.d.ts +17 -0
- package/dist/mcp-http-handler.js +144 -3
- package/dist/next-up-queue-store.d.ts +31 -0
- package/dist/next-up-queue-store.js +169 -0
- package/dist/openclaw.plugin.json +1 -1
- package/dist/outbox.d.ts +1 -1
- package/dist/runtime-instance-store.d.ts +1 -1
- package/dist/runtime-instance-store.js +20 -3
- package/dist/skill-pack-state.d.ts +69 -0
- package/dist/skill-pack-state.js +232 -0
- package/dist/worker-supervisor.d.ts +25 -0
- package/dist/worker-supervisor.js +62 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +10 -1
- package/skills/orgx-design-agent/SKILL.md +38 -0
- package/skills/orgx-engineering-agent/SKILL.md +55 -0
- package/skills/orgx-marketing-agent/SKILL.md +40 -0
- package/skills/orgx-operations-agent/SKILL.md +40 -0
- package/skills/orgx-orchestrator-agent/SKILL.md +45 -0
- package/skills/orgx-product-agent/SKILL.md +39 -0
- package/skills/orgx-sales-agent/SKILL.md +40 -0
- package/skills/ship/SKILL.md +63 -0
- package/dashboard/dist/assets/B68j2crt.js +0 -1
- package/dashboard/dist/assets/BZZ-fiJx.js +0 -32
- package/dashboard/dist/assets/BoXlCHKa.js +0 -9
- package/dashboard/dist/assets/Bq9x_Xyh.css +0 -1
- package/dashboard/dist/assets/DBhrRVdp.js +0 -1
- package/dashboard/dist/assets/DD1jv1Hd.js +0 -8
- package/dashboard/dist/assets/DNjbmawF.js +0 -214
package/dist/contracts/client.js
CHANGED
|
@@ -6,13 +6,43 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Uses native fetch — no external dependencies.
|
|
8
8
|
*/
|
|
9
|
-
const
|
|
9
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 25_000;
|
|
10
|
+
const DEFAULT_LIVE_TIMEOUT_MS = 30_000;
|
|
11
|
+
const DEFAULT_SYNC_TIMEOUT_MS = 45_000;
|
|
10
12
|
const USER_AGENT = "OrgX-Clawdbot-Plugin/1.0";
|
|
11
13
|
const DECISION_MUTATION_CONCURRENCY = 6;
|
|
12
14
|
const DEFAULT_CLIENT_BASE_URL = "https://www.useorgx.com";
|
|
13
15
|
function isUserScopedApiKey(apiKey) {
|
|
14
16
|
return apiKey.trim().toLowerCase().startsWith("oxk_");
|
|
15
17
|
}
|
|
18
|
+
function parseTimeoutMsEnv(name) {
|
|
19
|
+
const raw = process.env[name];
|
|
20
|
+
if (!raw)
|
|
21
|
+
return null;
|
|
22
|
+
const parsed = Number(raw);
|
|
23
|
+
if (!Number.isFinite(parsed))
|
|
24
|
+
return null;
|
|
25
|
+
// Keep it sane: timeouts below 1s tend to create pathological "offline" loops.
|
|
26
|
+
return Math.max(1_000, Math.floor(parsed));
|
|
27
|
+
}
|
|
28
|
+
function resolveRequestTimeoutMs(path) {
|
|
29
|
+
const configured = parseTimeoutMsEnv("ORGX_HTTP_TIMEOUT_MS") ??
|
|
30
|
+
parseTimeoutMsEnv("ORGX_API_TIMEOUT_MS");
|
|
31
|
+
const base = configured ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
32
|
+
// Live endpoints frequently return larger payloads (sessions, activity, agents).
|
|
33
|
+
if (path.startsWith("/api/client/live/")) {
|
|
34
|
+
return Math.max(base, DEFAULT_LIVE_TIMEOUT_MS);
|
|
35
|
+
}
|
|
36
|
+
// Sync can include a full org snapshot and may take longer than typical CRUD.
|
|
37
|
+
if (path === "/api/client/sync") {
|
|
38
|
+
return Math.max(base, DEFAULT_SYNC_TIMEOUT_MS);
|
|
39
|
+
}
|
|
40
|
+
// Handoffs can require server-side aggregation.
|
|
41
|
+
if (path === "/api/client/handoffs") {
|
|
42
|
+
return Math.max(base, DEFAULT_LIVE_TIMEOUT_MS);
|
|
43
|
+
}
|
|
44
|
+
return base;
|
|
45
|
+
}
|
|
16
46
|
function normalizeHost(value) {
|
|
17
47
|
return value.trim().toLowerCase().replace(/^\[|\]$/g, "");
|
|
18
48
|
}
|
|
@@ -85,7 +115,8 @@ export class OrgXClient {
|
|
|
85
115
|
async request(method, path, body) {
|
|
86
116
|
const url = `${this.baseUrl}${path}`;
|
|
87
117
|
const controller = new AbortController();
|
|
88
|
-
const
|
|
118
|
+
const timeoutMs = resolveRequestTimeoutMs(path);
|
|
119
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
89
120
|
try {
|
|
90
121
|
const headers = {
|
|
91
122
|
"Content-Type": "application/json",
|
|
@@ -124,7 +155,7 @@ export class OrgXClient {
|
|
|
124
155
|
}
|
|
125
156
|
catch (err) {
|
|
126
157
|
if (err instanceof DOMException && err.name === "AbortError") {
|
|
127
|
-
throw new Error(`OrgX API ${method} ${path} timed out after ${
|
|
158
|
+
throw new Error(`OrgX API ${method} ${path} timed out after ${timeoutMs}ms`);
|
|
128
159
|
}
|
|
129
160
|
throw err;
|
|
130
161
|
}
|
|
@@ -194,6 +225,72 @@ export class OrgXClient {
|
|
|
194
225
|
}
|
|
195
226
|
return response;
|
|
196
227
|
}
|
|
228
|
+
// ===========================================================================
|
|
229
|
+
// Kickoff Context
|
|
230
|
+
// ===========================================================================
|
|
231
|
+
async getKickoffContext(payload) {
|
|
232
|
+
return await this.post("/api/client/kickoff-context", payload ?? {});
|
|
233
|
+
}
|
|
234
|
+
// ===========================================================================
|
|
235
|
+
// Skill Packs (ETag-aware)
|
|
236
|
+
// ===========================================================================
|
|
237
|
+
async getSkillPack(input) {
|
|
238
|
+
const name = (input?.name ?? "").trim() || "orgx-agent-suite";
|
|
239
|
+
const url = `${this.baseUrl}/api/client/skill-pack?name=${encodeURIComponent(name)}`;
|
|
240
|
+
const controller = new AbortController();
|
|
241
|
+
const timeoutMs = resolveRequestTimeoutMs("/api/client/skill-pack");
|
|
242
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
243
|
+
try {
|
|
244
|
+
const headers = {
|
|
245
|
+
"Content-Type": "application/json",
|
|
246
|
+
"User-Agent": USER_AGENT,
|
|
247
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
248
|
+
};
|
|
249
|
+
if (this.userId && !isUserScopedApiKey(this.apiKey)) {
|
|
250
|
+
headers["X-Orgx-User-Id"] = this.userId;
|
|
251
|
+
}
|
|
252
|
+
const ifNoneMatch = (input?.ifNoneMatch ?? "").trim();
|
|
253
|
+
if (ifNoneMatch) {
|
|
254
|
+
headers["If-None-Match"] = ifNoneMatch;
|
|
255
|
+
}
|
|
256
|
+
const response = await fetch(url, {
|
|
257
|
+
method: "GET",
|
|
258
|
+
headers,
|
|
259
|
+
signal: controller.signal,
|
|
260
|
+
});
|
|
261
|
+
const etag = response.headers.get("etag");
|
|
262
|
+
if (response.status === 304) {
|
|
263
|
+
return { ok: true, notModified: true, etag, pack: null };
|
|
264
|
+
}
|
|
265
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
266
|
+
const payload = contentType.includes("application/json")
|
|
267
|
+
? (await response.json().catch(() => null))
|
|
268
|
+
: null;
|
|
269
|
+
if (!response.ok) {
|
|
270
|
+
const detail = payload && typeof payload === "object" && "error" in payload && typeof payload.error === "string"
|
|
271
|
+
? payload.error
|
|
272
|
+
: `${response.status} ${response.statusText}`;
|
|
273
|
+
return { ok: false, status: response.status, error: detail };
|
|
274
|
+
}
|
|
275
|
+
if (payload && typeof payload === "object" && payload.ok === true && payload.data) {
|
|
276
|
+
return { ok: true, notModified: false, etag, pack: payload.data };
|
|
277
|
+
}
|
|
278
|
+
return { ok: false, status: 502, error: "SkillPack response was invalid" };
|
|
279
|
+
}
|
|
280
|
+
catch (err) {
|
|
281
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
282
|
+
return {
|
|
283
|
+
ok: false,
|
|
284
|
+
status: 504,
|
|
285
|
+
error: `OrgX API GET /api/client/skill-pack timed out after ${timeoutMs}ms`,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
return { ok: false, status: 502, error: err instanceof Error ? err.message : "SkillPack request failed" };
|
|
289
|
+
}
|
|
290
|
+
finally {
|
|
291
|
+
clearTimeout(timeout);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
197
294
|
async delegationPreflight(payload) {
|
|
198
295
|
return this.post("/api/client/delegation/preflight", payload);
|
|
199
296
|
}
|
|
@@ -320,6 +417,26 @@ export class OrgXClient {
|
|
|
320
417
|
}
|
|
321
418
|
return response;
|
|
322
419
|
}
|
|
420
|
+
async recordRunOutcome(payload) {
|
|
421
|
+
const response = await this.post("/api/client/live/runs/outcomes/record", payload);
|
|
422
|
+
if (response &&
|
|
423
|
+
typeof response === "object" &&
|
|
424
|
+
"data" in response &&
|
|
425
|
+
response.data) {
|
|
426
|
+
return response.data;
|
|
427
|
+
}
|
|
428
|
+
return response;
|
|
429
|
+
}
|
|
430
|
+
async recordRunRetro(payload) {
|
|
431
|
+
const response = await this.post("/api/client/live/runs/retro", payload);
|
|
432
|
+
if (response &&
|
|
433
|
+
typeof response === "object" &&
|
|
434
|
+
"data" in response &&
|
|
435
|
+
response.data) {
|
|
436
|
+
return response.data;
|
|
437
|
+
}
|
|
438
|
+
return response;
|
|
439
|
+
}
|
|
323
440
|
// ===========================================================================
|
|
324
441
|
// Live Sessions + Activity + Handoffs
|
|
325
442
|
// ===========================================================================
|
|
@@ -15,6 +15,14 @@ export interface OrgXConfig {
|
|
|
15
15
|
syncIntervalMs: number;
|
|
16
16
|
/** Plugin enabled */
|
|
17
17
|
enabled: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* When true (default), provision/update the OrgX agent suite automatically
|
|
20
|
+
* after a successful connection/sync.
|
|
21
|
+
*
|
|
22
|
+
* Safe by default: provisioning uses managed/local overlays and skips files
|
|
23
|
+
* that appear to have out-of-band edits ("conflict").
|
|
24
|
+
*/
|
|
25
|
+
autoInstallAgentSuiteOnConnect?: boolean;
|
|
18
26
|
}
|
|
19
27
|
export type OnboardingStatus = 'idle' | 'starting' | 'awaiting_browser_auth' | 'pairing' | 'connected' | 'error' | 'manual_key';
|
|
20
28
|
export type OnboardingNextAction = 'connect' | 'wait_for_browser' | 'open_dashboard' | 'enter_manual_key' | 'retry' | 'reconnect';
|
|
@@ -45,6 +53,119 @@ export interface OrgSnapshot {
|
|
|
45
53
|
/** Last sync timestamp */
|
|
46
54
|
syncedAt: string;
|
|
47
55
|
}
|
|
56
|
+
export type KickoffContextScope = {
|
|
57
|
+
initiative_id?: string | null;
|
|
58
|
+
workstream_id?: string | null;
|
|
59
|
+
task_id?: string | null;
|
|
60
|
+
};
|
|
61
|
+
export type KickoffContextEntityRef = {
|
|
62
|
+
id: string;
|
|
63
|
+
title: string;
|
|
64
|
+
status?: string | null;
|
|
65
|
+
summary?: string | null;
|
|
66
|
+
url?: string | null;
|
|
67
|
+
metadata?: Record<string, unknown> | null;
|
|
68
|
+
};
|
|
69
|
+
export type KickoffContextToolScope = {
|
|
70
|
+
allow?: string[];
|
|
71
|
+
deny?: string[];
|
|
72
|
+
notes?: string | null;
|
|
73
|
+
};
|
|
74
|
+
export interface KickoffContext {
|
|
75
|
+
/** Deterministic hash of the payload used to render a kickoff message. */
|
|
76
|
+
context_hash: string;
|
|
77
|
+
/** Optional schema version for forward/back compat. */
|
|
78
|
+
schema_version?: string | null;
|
|
79
|
+
/** Human-friendly overview sentence(s). */
|
|
80
|
+
overview?: string | null;
|
|
81
|
+
initiative?: KickoffContextEntityRef | null;
|
|
82
|
+
workstream?: KickoffContextEntityRef | null;
|
|
83
|
+
task?: (KickoffContextEntityRef & {
|
|
84
|
+
description?: string | null;
|
|
85
|
+
checklist?: string[] | null;
|
|
86
|
+
}) | null;
|
|
87
|
+
acceptance_criteria?: string[] | null;
|
|
88
|
+
constraints?: string[] | null;
|
|
89
|
+
risks?: string[] | null;
|
|
90
|
+
decisions?: KickoffContextEntityRef[] | null;
|
|
91
|
+
artifacts?: KickoffContextEntityRef[] | null;
|
|
92
|
+
tool_scope?: KickoffContextToolScope | null;
|
|
93
|
+
reporting_expectations?: string[] | null;
|
|
94
|
+
/** Server-provided hints for agent tone/behavior; optional. */
|
|
95
|
+
persona?: {
|
|
96
|
+
voice?: string | null;
|
|
97
|
+
collaboration_style?: string | null;
|
|
98
|
+
defaults?: string[] | null;
|
|
99
|
+
} | null;
|
|
100
|
+
}
|
|
101
|
+
export type KickoffContextRequest = KickoffContextScope & {
|
|
102
|
+
agent_id?: string | null;
|
|
103
|
+
domain?: string | null;
|
|
104
|
+
required_skills?: string[] | null;
|
|
105
|
+
message?: string | null;
|
|
106
|
+
};
|
|
107
|
+
export type KickoffContextResponse = {
|
|
108
|
+
ok: true;
|
|
109
|
+
data: KickoffContext;
|
|
110
|
+
} | {
|
|
111
|
+
ok: false;
|
|
112
|
+
error: string;
|
|
113
|
+
};
|
|
114
|
+
export type OrgxAgentDomain = "engineering" | "product" | "design" | "marketing" | "sales" | "operations" | "orchestration";
|
|
115
|
+
/**
|
|
116
|
+
* AgentProfile describes the stable agent identity + workspace configuration
|
|
117
|
+
* needed to instantiate OrgX agents in OpenClaw.
|
|
118
|
+
*
|
|
119
|
+
* Note: this is a provisioning contract (what to install/configure), not an
|
|
120
|
+
* execution record (runs/sessions).
|
|
121
|
+
*/
|
|
122
|
+
export type OrgxAgentProfile = {
|
|
123
|
+
id: string;
|
|
124
|
+
name: string;
|
|
125
|
+
domain: OrgxAgentDomain;
|
|
126
|
+
workspace: string;
|
|
127
|
+
required_skills?: string[] | null;
|
|
128
|
+
tool_scope?: KickoffContextToolScope | null;
|
|
129
|
+
persona?: KickoffContext["persona"] | null;
|
|
130
|
+
};
|
|
131
|
+
export type OrgxAgentPack = {
|
|
132
|
+
pack_id: string;
|
|
133
|
+
pack_version: string;
|
|
134
|
+
schema_version?: string | null;
|
|
135
|
+
skill_pack?: {
|
|
136
|
+
name: string;
|
|
137
|
+
version: string;
|
|
138
|
+
checksum: string;
|
|
139
|
+
} | null;
|
|
140
|
+
agents: OrgxAgentProfile[];
|
|
141
|
+
managed_files: string[];
|
|
142
|
+
};
|
|
143
|
+
export type SkillPack = {
|
|
144
|
+
name: string;
|
|
145
|
+
version: string;
|
|
146
|
+
checksum: string;
|
|
147
|
+
status?: "draft" | "pending_review" | "approved" | "rejected" | string;
|
|
148
|
+
manifest: Record<string, unknown>;
|
|
149
|
+
required_scopes?: string[] | null;
|
|
150
|
+
required_tools?: string[] | null;
|
|
151
|
+
updated_at?: string | null;
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* Canonical manifest shape expected by the OpenClaw plugin.
|
|
155
|
+
*
|
|
156
|
+
* Stored in `skill_packs.manifest` on the OrgX server.
|
|
157
|
+
*/
|
|
158
|
+
export type OpenClawSkillPackManifestV1 = {
|
|
159
|
+
schema_version: string;
|
|
160
|
+
openclaw_skills: Partial<Record<OrgxAgentDomain, string>>;
|
|
161
|
+
};
|
|
162
|
+
export type SkillPackResponse = {
|
|
163
|
+
ok: true;
|
|
164
|
+
data: SkillPack;
|
|
165
|
+
} | {
|
|
166
|
+
ok: false;
|
|
167
|
+
error: string;
|
|
168
|
+
};
|
|
48
169
|
export interface Initiative {
|
|
49
170
|
id: string;
|
|
50
171
|
title: string;
|
|
@@ -349,13 +470,81 @@ export interface ApplyChangesetResponse {
|
|
|
349
470
|
event_id: string | null;
|
|
350
471
|
auth_mode?: 'service' | 'api_key';
|
|
351
472
|
}
|
|
473
|
+
export interface RecordRunOutcomeRequest {
|
|
474
|
+
initiative_id: string;
|
|
475
|
+
execution_id: string;
|
|
476
|
+
execution_type: string;
|
|
477
|
+
agent_id: string;
|
|
478
|
+
task_type?: string;
|
|
479
|
+
domain?: string;
|
|
480
|
+
started_at?: string;
|
|
481
|
+
completed_at?: string;
|
|
482
|
+
inputs?: Record<string, unknown>;
|
|
483
|
+
outputs?: Record<string, unknown>;
|
|
484
|
+
steps?: Array<Record<string, unknown>>;
|
|
485
|
+
success: boolean;
|
|
486
|
+
quality_score?: number;
|
|
487
|
+
duration_vs_estimate?: number;
|
|
488
|
+
cost_vs_budget?: number;
|
|
489
|
+
human_interventions?: number;
|
|
490
|
+
user_satisfaction?: number;
|
|
491
|
+
errors?: string[];
|
|
492
|
+
metadata?: Record<string, unknown>;
|
|
493
|
+
run_id?: string;
|
|
494
|
+
correlation_id?: string;
|
|
495
|
+
source_client?: ReportingSourceClient;
|
|
496
|
+
}
|
|
497
|
+
export interface RecordRunOutcomeResponse {
|
|
498
|
+
ok: true;
|
|
499
|
+
run_id: string;
|
|
500
|
+
reused_run: boolean;
|
|
501
|
+
execution_id: string;
|
|
502
|
+
event_id: string | null;
|
|
503
|
+
auth_mode?: 'service' | 'api_key';
|
|
504
|
+
}
|
|
505
|
+
export type RetroFollowUpPriority = 'p0' | 'p1' | 'p2';
|
|
506
|
+
export interface RetroJson {
|
|
507
|
+
summary: string;
|
|
508
|
+
what_went_well?: string[];
|
|
509
|
+
what_went_wrong?: string[];
|
|
510
|
+
decisions?: string[];
|
|
511
|
+
follow_ups?: Array<{
|
|
512
|
+
title: string;
|
|
513
|
+
priority?: RetroFollowUpPriority;
|
|
514
|
+
reason?: string;
|
|
515
|
+
}>;
|
|
516
|
+
signals?: Record<string, unknown>;
|
|
517
|
+
}
|
|
518
|
+
export type RetroEntityType = 'initiative' | 'workstream' | 'milestone' | 'task';
|
|
519
|
+
export interface RecordRunRetroRequest {
|
|
520
|
+
initiative_id: string;
|
|
521
|
+
entity_type?: RetroEntityType;
|
|
522
|
+
entity_id?: string;
|
|
523
|
+
title?: string;
|
|
524
|
+
idempotency_key?: string;
|
|
525
|
+
retro: RetroJson;
|
|
526
|
+
markdown?: string;
|
|
527
|
+
run_id?: string;
|
|
528
|
+
correlation_id?: string;
|
|
529
|
+
source_client?: ReportingSourceClient;
|
|
530
|
+
}
|
|
531
|
+
export interface RecordRunRetroResponse {
|
|
532
|
+
ok: true;
|
|
533
|
+
run_id: string;
|
|
534
|
+
reused_run: boolean;
|
|
535
|
+
work_artifact_id: string | null;
|
|
536
|
+
run_step_id: string | null;
|
|
537
|
+
run_artifact_id: string | null;
|
|
538
|
+
event_id: string | null;
|
|
539
|
+
auth_mode?: 'service' | 'api_key';
|
|
540
|
+
}
|
|
352
541
|
export type LiveActivityType = 'run_started' | 'run_completed' | 'run_failed' | 'artifact_created' | 'decision_requested' | 'decision_resolved' | 'handoff_requested' | 'handoff_claimed' | 'handoff_fulfilled' | 'blocker_created' | 'milestone_completed' | 'delegation';
|
|
353
542
|
export type RuntimeInstanceState = 'active' | 'stale' | 'stopped' | 'error';
|
|
354
543
|
export interface RuntimeInstance {
|
|
355
544
|
id: string;
|
|
356
545
|
sourceClient: RuntimeSourceClient;
|
|
357
546
|
displayName: string;
|
|
358
|
-
providerLogo: 'openai' | 'anthropic' | 'openclaw' | 'orgx' | 'unknown';
|
|
547
|
+
providerLogo: 'codex' | 'openai' | 'anthropic' | 'openclaw' | 'orgx' | 'unknown';
|
|
359
548
|
state: RuntimeInstanceState;
|
|
360
549
|
runId: string | null;
|
|
361
550
|
correlationId: string | null;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type EntityCommentAuthorType = "human" | "agent" | "system";
|
|
2
|
+
export type EntityCommentRecord = {
|
|
3
|
+
id: string;
|
|
4
|
+
parent_comment_id: string | null;
|
|
5
|
+
author_type: EntityCommentAuthorType;
|
|
6
|
+
author_id: string;
|
|
7
|
+
author_name: string | null;
|
|
8
|
+
body: string;
|
|
9
|
+
comment_type: string;
|
|
10
|
+
severity: string;
|
|
11
|
+
tags: string[] | null;
|
|
12
|
+
created_at: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function listEntityComments(entityType: string, entityId: string): EntityCommentRecord[];
|
|
15
|
+
export declare function appendEntityComment(input: {
|
|
16
|
+
entityType: string;
|
|
17
|
+
entityId: string;
|
|
18
|
+
body: string;
|
|
19
|
+
commentType?: string | null;
|
|
20
|
+
severity?: string | null;
|
|
21
|
+
tags?: unknown;
|
|
22
|
+
author?: {
|
|
23
|
+
author_type?: EntityCommentAuthorType;
|
|
24
|
+
author_id?: string;
|
|
25
|
+
author_name?: string | null;
|
|
26
|
+
};
|
|
27
|
+
}): EntityCommentRecord;
|
|
28
|
+
export declare function mergeEntityComments(remote: unknown, local: EntityCommentRecord[]): EntityCommentRecord[];
|
|
29
|
+
export declare function clearEntityCommentsStore(): void;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, } from "node:fs";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { getOrgxPluginConfigDir, getOrgxPluginConfigPath } from "./paths.js";
|
|
4
|
+
import { backupCorruptFileSync, writeJsonFileAtomicSync } from "./fs-utils.js";
|
|
5
|
+
const MAX_COMMENTS_PER_ENTITY = 240;
|
|
6
|
+
const MAX_TOTAL_COMMENTS = 1_500;
|
|
7
|
+
function commentsDir() {
|
|
8
|
+
return getOrgxPluginConfigDir();
|
|
9
|
+
}
|
|
10
|
+
function commentsFile() {
|
|
11
|
+
return getOrgxPluginConfigPath("entity-comments.json");
|
|
12
|
+
}
|
|
13
|
+
function ensureDir() {
|
|
14
|
+
const dir = commentsDir();
|
|
15
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
16
|
+
try {
|
|
17
|
+
chmodSync(dir, 0o700);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// best effort
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function parseJson(value) {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(value);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function entityKey(entityType, entityId) {
|
|
32
|
+
return `${entityType.trim().toLowerCase()}:${entityId.trim()}`;
|
|
33
|
+
}
|
|
34
|
+
function normalizeTags(value) {
|
|
35
|
+
if (!Array.isArray(value))
|
|
36
|
+
return null;
|
|
37
|
+
const tags = value
|
|
38
|
+
.map((item) => (typeof item === "string" ? item.trim() : ""))
|
|
39
|
+
.filter((item) => item.length > 0);
|
|
40
|
+
return tags.length > 0 ? tags.slice(0, 16) : [];
|
|
41
|
+
}
|
|
42
|
+
function normalizeComment(input) {
|
|
43
|
+
const createdAt = typeof input.created_at === "string" ? input.created_at : new Date().toISOString();
|
|
44
|
+
return {
|
|
45
|
+
id: typeof input.id === "string" && input.id.trim().length > 0 ? input.id.trim() : `local_${randomUUID()}`,
|
|
46
|
+
parent_comment_id: typeof input.parent_comment_id === "string" && input.parent_comment_id.trim().length > 0
|
|
47
|
+
? input.parent_comment_id.trim()
|
|
48
|
+
: null,
|
|
49
|
+
author_type: input.author_type === "agent" || input.author_type === "system" ? input.author_type : "human",
|
|
50
|
+
author_id: typeof input.author_id === "string" && input.author_id.trim().length > 0 ? input.author_id.trim() : "local_user",
|
|
51
|
+
author_name: typeof input.author_name === "string" && input.author_name.trim().length > 0 ? input.author_name.trim() : null,
|
|
52
|
+
body: typeof input.body === "string" ? input.body : "",
|
|
53
|
+
comment_type: typeof input.comment_type === "string" && input.comment_type.trim().length > 0 ? input.comment_type.trim() : "note",
|
|
54
|
+
severity: typeof input.severity === "string" && input.severity.trim().length > 0 ? input.severity.trim() : "info",
|
|
55
|
+
tags: normalizeTags(input.tags) ?? [],
|
|
56
|
+
created_at: createdAt,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function readStore() {
|
|
60
|
+
const file = commentsFile();
|
|
61
|
+
try {
|
|
62
|
+
if (!existsSync(file)) {
|
|
63
|
+
return { updatedAt: new Date().toISOString(), commentsByEntity: {} };
|
|
64
|
+
}
|
|
65
|
+
const raw = readFileSync(file, "utf8");
|
|
66
|
+
const parsed = parseJson(raw);
|
|
67
|
+
if (!parsed || typeof parsed !== "object") {
|
|
68
|
+
backupCorruptFileSync(file);
|
|
69
|
+
return { updatedAt: new Date().toISOString(), commentsByEntity: {} };
|
|
70
|
+
}
|
|
71
|
+
const commentsByEntity = parsed.commentsByEntity && typeof parsed.commentsByEntity === "object"
|
|
72
|
+
? parsed.commentsByEntity
|
|
73
|
+
: {};
|
|
74
|
+
return {
|
|
75
|
+
updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : new Date().toISOString(),
|
|
76
|
+
commentsByEntity,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return { updatedAt: new Date().toISOString(), commentsByEntity: {} };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function writeStore(store) {
|
|
84
|
+
ensureDir();
|
|
85
|
+
const file = commentsFile();
|
|
86
|
+
writeJsonFileAtomicSync(file, store, 0o600);
|
|
87
|
+
}
|
|
88
|
+
function trimStore(store) {
|
|
89
|
+
const next = {
|
|
90
|
+
updatedAt: store.updatedAt,
|
|
91
|
+
commentsByEntity: { ...store.commentsByEntity },
|
|
92
|
+
};
|
|
93
|
+
for (const [key, comments] of Object.entries(next.commentsByEntity)) {
|
|
94
|
+
if (!Array.isArray(comments) || comments.length === 0) {
|
|
95
|
+
delete next.commentsByEntity[key];
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const normalized = comments.map((c) => normalizeComment(c));
|
|
99
|
+
normalized.sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at));
|
|
100
|
+
next.commentsByEntity[key] = normalized.slice(-MAX_COMMENTS_PER_ENTITY);
|
|
101
|
+
}
|
|
102
|
+
const all = [];
|
|
103
|
+
for (const [key, comments] of Object.entries(next.commentsByEntity)) {
|
|
104
|
+
for (const comment of comments)
|
|
105
|
+
all.push({ key, comment });
|
|
106
|
+
}
|
|
107
|
+
if (all.length <= MAX_TOTAL_COMMENTS)
|
|
108
|
+
return next;
|
|
109
|
+
all.sort((a, b) => Date.parse(b.comment.created_at) - Date.parse(a.comment.created_at));
|
|
110
|
+
const keep = all.slice(0, MAX_TOTAL_COMMENTS);
|
|
111
|
+
const keepByEntity = new Map();
|
|
112
|
+
for (const item of keep) {
|
|
113
|
+
const list = keepByEntity.get(item.key) ?? [];
|
|
114
|
+
list.push(item.comment);
|
|
115
|
+
keepByEntity.set(item.key, list);
|
|
116
|
+
}
|
|
117
|
+
const rebuilt = {};
|
|
118
|
+
for (const [key, list] of keepByEntity.entries()) {
|
|
119
|
+
list.sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at));
|
|
120
|
+
rebuilt[key] = list;
|
|
121
|
+
}
|
|
122
|
+
next.commentsByEntity = rebuilt;
|
|
123
|
+
return next;
|
|
124
|
+
}
|
|
125
|
+
export function listEntityComments(entityType, entityId) {
|
|
126
|
+
const key = entityKey(entityType, entityId);
|
|
127
|
+
const store = readStore();
|
|
128
|
+
const list = store.commentsByEntity[key];
|
|
129
|
+
if (!Array.isArray(list) || list.length === 0)
|
|
130
|
+
return [];
|
|
131
|
+
return list.map((c) => normalizeComment(c));
|
|
132
|
+
}
|
|
133
|
+
export function appendEntityComment(input) {
|
|
134
|
+
const body = input.body.trim();
|
|
135
|
+
if (!body) {
|
|
136
|
+
throw new Error("comment body is required");
|
|
137
|
+
}
|
|
138
|
+
const key = entityKey(input.entityType, input.entityId);
|
|
139
|
+
const now = new Date().toISOString();
|
|
140
|
+
const record = normalizeComment({
|
|
141
|
+
id: `local_${randomUUID()}`,
|
|
142
|
+
parent_comment_id: null,
|
|
143
|
+
author_type: input.author?.author_type ?? "human",
|
|
144
|
+
author_id: input.author?.author_id ?? "local_user",
|
|
145
|
+
author_name: input.author?.author_name ?? null,
|
|
146
|
+
body,
|
|
147
|
+
comment_type: (input.commentType ?? "note") || "note",
|
|
148
|
+
severity: (input.severity ?? "info") || "info",
|
|
149
|
+
tags: normalizeTags(input.tags) ?? [],
|
|
150
|
+
created_at: now,
|
|
151
|
+
});
|
|
152
|
+
const store = readStore();
|
|
153
|
+
const existing = Array.isArray(store.commentsByEntity[key]) ? store.commentsByEntity[key] : [];
|
|
154
|
+
const next = {
|
|
155
|
+
updatedAt: now,
|
|
156
|
+
commentsByEntity: {
|
|
157
|
+
...store.commentsByEntity,
|
|
158
|
+
[key]: [...existing, record],
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
const trimmed = trimStore(next);
|
|
162
|
+
writeStore(trimmed);
|
|
163
|
+
return record;
|
|
164
|
+
}
|
|
165
|
+
export function mergeEntityComments(remote, local) {
|
|
166
|
+
const remoteList = Array.isArray(remote) ? remote : [];
|
|
167
|
+
const merged = new Map();
|
|
168
|
+
for (const comment of remoteList) {
|
|
169
|
+
if (!comment || typeof comment !== "object")
|
|
170
|
+
continue;
|
|
171
|
+
const record = normalizeComment(comment);
|
|
172
|
+
merged.set(record.id, record);
|
|
173
|
+
}
|
|
174
|
+
for (const comment of local) {
|
|
175
|
+
const record = normalizeComment(comment);
|
|
176
|
+
merged.set(record.id, record);
|
|
177
|
+
}
|
|
178
|
+
const list = Array.from(merged.values());
|
|
179
|
+
list.sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at));
|
|
180
|
+
return list;
|
|
181
|
+
}
|
|
182
|
+
export function clearEntityCommentsStore() {
|
|
183
|
+
const file = commentsFile();
|
|
184
|
+
try {
|
|
185
|
+
rmSync(file, { force: true });
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// best effort
|
|
189
|
+
}
|
|
190
|
+
}
|