@useorgx/openclaw-plugin 0.4.5 → 0.4.8
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 +333 -26
- package/dashboard/dist/assets/B3ziCA02.js +8 -0
- package/dashboard/dist/assets/BNeJ0kpF.js +1 -0
- package/dashboard/dist/assets/BzkiMPmM.js +215 -0
- package/dashboard/dist/assets/CUV9IHHi.js +1 -0
- package/dashboard/dist/assets/CpJsfbXo.js +9 -0
- package/dashboard/dist/assets/Ie7d9Iq2.css +1 -0
- package/dashboard/dist/assets/sAhvFnpk.js +4 -0
- package/dashboard/dist/index.html +5 -5
- package/dist/activity-actor-fields.d.ts +3 -0
- package/dist/activity-actor-fields.js +128 -0
- package/dist/activity-store.d.ts +28 -0
- package/dist/activity-store.js +257 -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/artifacts/register-artifact.d.ts +47 -0
- package/dist/artifacts/register-artifact.js +271 -0
- package/dist/auth-store.js +8 -13
- package/dist/contracts/client.d.ts +23 -1
- package/dist/contracts/client.js +127 -8
- package/dist/contracts/types.d.ts +194 -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 +4500 -534
- package/dist/index.js +1078 -68
- package/dist/local-openclaw.js +8 -0
- package/dist/mcp-client-setup.js +145 -28
- 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 +19 -2
- 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 +77 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +15 -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
|
@@ -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;
|
|
@@ -379,6 +568,10 @@ export interface LiveActivityItem {
|
|
|
379
568
|
description: string | null;
|
|
380
569
|
agentId: string | null;
|
|
381
570
|
agentName: string | null;
|
|
571
|
+
requesterAgentId: string | null;
|
|
572
|
+
requesterAgentName: string | null;
|
|
573
|
+
executorAgentId: string | null;
|
|
574
|
+
executorAgentName: string | null;
|
|
382
575
|
runId: string | null;
|
|
383
576
|
initiativeId: string | null;
|
|
384
577
|
timestamp: string;
|
|
@@ -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
|
+
}
|