@cryptolibertus/pi-peer 0.3.2
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/LICENSE +21 -0
- package/README.md +70 -0
- package/extensions/pi-peer/index.ts +753 -0
- package/package.json +58 -0
- package/src/peers/command.mjs +289 -0
- package/src/peers/comms.mjs +676 -0
- package/src/peers/config.mjs +356 -0
- package/src/peers/extension-lifecycle.mjs +21 -0
- package/src/peers/goal-board.mjs +528 -0
- package/src/peers/guidance.mjs +45 -0
- package/src/peers/inbound-bridge.mjs +240 -0
- package/src/peers/local-transport.mjs +814 -0
- package/src/peers/message-store.mjs +114 -0
- package/src/peers/protocol.mjs +256 -0
- package/src/peers/role-collaboration-demo.mjs +71 -0
- package/src/peers/runtime.mjs +200 -0
- package/src/peers/status.mjs +158 -0
- package/src/peers/tool-results.mjs +154 -0
- package/src/utils.mjs +83 -0
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cryptolibertus/pi-peer",
|
|
3
|
+
"version": "0.3.2",
|
|
4
|
+
"description": "Pi package for local Pi-to-Pi peer messaging, slash commands, tools, and runtime transport.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"pi-package",
|
|
9
|
+
"pi-extension",
|
|
10
|
+
"pi-peer",
|
|
11
|
+
"peer-messaging",
|
|
12
|
+
"agent-collaboration"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
"extensions",
|
|
16
|
+
"src",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"test": "node --test ../../test/peer-*.test.mjs",
|
|
22
|
+
"check": "npm test && npm run check:pack",
|
|
23
|
+
"check:pack": "npm pack --dry-run",
|
|
24
|
+
"smoke:pack": "npm run check:pack",
|
|
25
|
+
"prepublishOnly": "npm run check"
|
|
26
|
+
},
|
|
27
|
+
"exports": {
|
|
28
|
+
".": "./src/peers/comms.mjs",
|
|
29
|
+
"./extension": "./extensions/pi-peer/index.ts",
|
|
30
|
+
"./peers/*": "./src/peers/*.mjs"
|
|
31
|
+
},
|
|
32
|
+
"pi": {
|
|
33
|
+
"extensions": [
|
|
34
|
+
"extensions/pi-peer/index.ts"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=20"
|
|
39
|
+
},
|
|
40
|
+
"packageManager": "npm@11.12.1",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/CryptoLibertus/pi-peer.git"
|
|
44
|
+
},
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/CryptoLibertus/pi-peer/issues"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/CryptoLibertus/pi-peer#readme",
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"@earendil-works/pi-ai": "*",
|
|
54
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
55
|
+
"@earendil-works/pi-tui": "*",
|
|
56
|
+
"typebox": "*"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { flagEnabled, parseFlags, splitCommandLine } from "../utils.mjs";
|
|
2
|
+
|
|
3
|
+
export const PEER_COMMANDS = Object.freeze(["help", "status", "list", "init", "setup", "doctor", "reconnect", "resume", "cancel", "send", "get", "await", "progress", "goal"]);
|
|
4
|
+
|
|
5
|
+
const PEER_GOAL_ALIASES = Object.freeze({
|
|
6
|
+
goals: ["list"],
|
|
7
|
+
ls: ["list"],
|
|
8
|
+
current: ["show"],
|
|
9
|
+
fanout: ["fanout"],
|
|
10
|
+
claim: ["claim"],
|
|
11
|
+
take: ["claim"],
|
|
12
|
+
heartbeat: ["heartbeat"],
|
|
13
|
+
ping: ["heartbeat"],
|
|
14
|
+
release: ["release"],
|
|
15
|
+
drop: ["release"],
|
|
16
|
+
finding: ["finding"],
|
|
17
|
+
note: ["note"],
|
|
18
|
+
handoff: ["handoff"],
|
|
19
|
+
done: ["handoff", "--status", "done"],
|
|
20
|
+
complete: ["handoff", "--status", "done"],
|
|
21
|
+
block: ["object"],
|
|
22
|
+
objection: ["object"],
|
|
23
|
+
object: ["object"],
|
|
24
|
+
resolve: ["resolve"],
|
|
25
|
+
unblock: ["resolve"],
|
|
26
|
+
vote: ["vote"],
|
|
27
|
+
close: ["close"],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export function parsePeerCommand(rawArgs = "") {
|
|
31
|
+
const parts = splitCommandLine(rawArgs);
|
|
32
|
+
const first = parts[0] && !parts[0].startsWith("--") ? parts.shift() : "status";
|
|
33
|
+
if (first === "pass" || first === "fail") {
|
|
34
|
+
const [goalId, ...rest] = parts;
|
|
35
|
+
const aliasParts = ["vote", goalId, first, ...rest].filter(Boolean);
|
|
36
|
+
const { flags, positionals } = parseFlags(aliasParts);
|
|
37
|
+
return parsePeerGoalCommand({ subcommand: "goal", flags, positionals, rawArgs }, flags, positionals);
|
|
38
|
+
}
|
|
39
|
+
if (first && PEER_GOAL_ALIASES[first]) {
|
|
40
|
+
const aliasParts = [...PEER_GOAL_ALIASES[first], ...parts];
|
|
41
|
+
const { flags, positionals } = parseFlags(aliasParts);
|
|
42
|
+
return parsePeerGoalCommand({ subcommand: "goal", flags, positionals, rawArgs }, flags, positionals);
|
|
43
|
+
}
|
|
44
|
+
const subcommand = PEER_COMMANDS.includes(first || "") ? first || "status" : "help";
|
|
45
|
+
const { flags, positionals } = parseFlags(parts);
|
|
46
|
+
const parsed = { subcommand, flags, positionals, rawArgs };
|
|
47
|
+
|
|
48
|
+
if (first && !PEER_COMMANDS.includes(first)) return { ...parsed, error: `Unknown /peer command '${first}'` };
|
|
49
|
+
if (subcommand === "send") {
|
|
50
|
+
const peerId = positionals[0];
|
|
51
|
+
const prompt = positionals.slice(1).join(" ").trim();
|
|
52
|
+
if (!peerId || !prompt) return { ...parsed, error: "/peer send requires <peer> <prompt>" };
|
|
53
|
+
const goalId = stringFlag(flags.goal || flags.goalId, undefined);
|
|
54
|
+
const claimedPaths = claimedPathsFlag(flags.claim || flags.claimedPath || flags.claimedPaths);
|
|
55
|
+
return {
|
|
56
|
+
...parsed,
|
|
57
|
+
peerId,
|
|
58
|
+
prompt,
|
|
59
|
+
intent: stringFlag(flags.intent, "ask"),
|
|
60
|
+
awaitResponse: !flagEnabled(flags.noAwait) && stringFlag(flags.await, "true") !== "false",
|
|
61
|
+
timeoutMs: positiveIntegerFlag(flags.timeoutMs),
|
|
62
|
+
maxHopCount: positiveIntegerFlag(flags.maxHopCount),
|
|
63
|
+
allowSelf: flagEnabled(flags.allowSelf),
|
|
64
|
+
goalId,
|
|
65
|
+
goalClaimMode: stringFlag(flags.claimMode, "write"),
|
|
66
|
+
goalStaleAfterMs: positiveIntegerFlag(flags.staleAfterMs),
|
|
67
|
+
claimedPaths,
|
|
68
|
+
metadata: metadataFromFlags(flags, { goalId, claimedPaths }),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (subcommand === "goal") {
|
|
72
|
+
return parsePeerGoalCommand(parsed, flags, positionals);
|
|
73
|
+
}
|
|
74
|
+
if (subcommand === "progress") {
|
|
75
|
+
const summary = positionals.join(" ").trim();
|
|
76
|
+
if (!summary) return { ...parsed, error: "/peer progress requires <summary>" };
|
|
77
|
+
return { ...parsed, summary, status: stringFlag(flags.status, undefined), phase: stringFlag(flags.phase, undefined), detail: stringFlag(flags.detail, undefined) };
|
|
78
|
+
}
|
|
79
|
+
if (subcommand === "resume") {
|
|
80
|
+
const messageId = positionals[0];
|
|
81
|
+
if (!messageId) return { ...parsed, error: "/peer resume requires <message-id>" };
|
|
82
|
+
return { ...parsed, messageId };
|
|
83
|
+
}
|
|
84
|
+
if (subcommand === "cancel") {
|
|
85
|
+
const messageId = positionals[0];
|
|
86
|
+
const reason = positionals.slice(1).join(" ").trim() || "cancelled by sender";
|
|
87
|
+
if (!messageId) return { ...parsed, error: "/peer cancel requires <message-id> [reason]" };
|
|
88
|
+
return { ...parsed, messageId, reason };
|
|
89
|
+
}
|
|
90
|
+
if (subcommand === "get") {
|
|
91
|
+
const id = positionals[0];
|
|
92
|
+
if (!id) return { ...parsed, error: "/peer get requires <id>" };
|
|
93
|
+
return { ...parsed, id };
|
|
94
|
+
}
|
|
95
|
+
if (subcommand === "await") {
|
|
96
|
+
const messageIds = positionals.filter(Boolean);
|
|
97
|
+
if (messageIds.length === 0) return { ...parsed, error: "/peer await requires <message-id> [message-id...]" };
|
|
98
|
+
return { ...parsed, messageIds, timeoutMs: positiveIntegerFlag(flags.timeoutMs) };
|
|
99
|
+
}
|
|
100
|
+
if (subcommand === "init" || subcommand === "setup") {
|
|
101
|
+
const localPeerId = stringFlag(flags.id || flags.localPeerId, undefined);
|
|
102
|
+
const role = stringFlag(flags.role, undefined);
|
|
103
|
+
const persona = stringFlag(flags.persona, undefined);
|
|
104
|
+
const trust = stringFlag(flags.trust, undefined);
|
|
105
|
+
const capabilities = capabilitiesFromFlags(flags);
|
|
106
|
+
const peer = stringFlag(flags.peer, undefined);
|
|
107
|
+
const peerRole = stringFlag(flags.peerRole, undefined);
|
|
108
|
+
const peerTrust = stringFlag(flags.peerTrust, undefined);
|
|
109
|
+
const peerCapabilities = capabilitiesFromFlags({ intents: flags.peerIntents });
|
|
110
|
+
const seedPeers = peer ? { [peer]: { ...(peerRole ? { role: peerRole } : {}), ...(peerTrust ? { trust: peerTrust } : {}), ...(Object.keys(peerCapabilities).length ? { capabilities: peerCapabilities } : {}) } } : undefined;
|
|
111
|
+
return stripUndefined({
|
|
112
|
+
...parsed,
|
|
113
|
+
localPeerId,
|
|
114
|
+
role,
|
|
115
|
+
persona,
|
|
116
|
+
trust,
|
|
117
|
+
...(Object.keys(capabilities).length ? { capabilities } : {}),
|
|
118
|
+
seedPeers,
|
|
119
|
+
enabled: !flagEnabled(flags.disabled),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return parsed;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function formatPeerHelp() {
|
|
126
|
+
return [
|
|
127
|
+
"# Peer Commands",
|
|
128
|
+
"",
|
|
129
|
+
"- `/peer status` — show local peer runtime, endpoint/auth, discovered peers, pending messages, and warnings",
|
|
130
|
+
"- `/peer list` — list configured and discovered peers",
|
|
131
|
+
"- `/peer setup [--id <peer-id>] [--role planner|worker|reviewer] [--peer <peer-id>]` — guided alias for creating .pi/peers.json with protocol/capability metadata; never overwrites",
|
|
132
|
+
"- `/peer init [--id <peer-id>]` — create .pi/peers.json if missing; never overwrites",
|
|
133
|
+
"- `/peer doctor` — check peer config, protocol compatibility, endpoint, discovered peers, and resumable tasks",
|
|
134
|
+
"- `/peer reconnect` — refresh local discovery and show current status",
|
|
135
|
+
"- `/peer resume <message-id>` — resume a disconnected restored peer message after reconnect",
|
|
136
|
+
"- `/peer cancel <message-id> [reason]` — mark a queued/running/disconnected peer message cancelled",
|
|
137
|
+
"- `/peer send <peer> <prompt> [--no-await] [--intent ask] [--goal <goal-id>] [--claim <path[,path]>] [--timeout-ms <ms>] [--allow-self]` — send a prompt-first peer message",
|
|
138
|
+
"- `/peer progress <summary> [--status running] [--phase <name>]` — send a structured checkpoint from an inbound long-running peer task",
|
|
139
|
+
"- `/peer goals|ls`, `/peer current [goal-id]`, `/peer fanout`, `/peer take|claim`, `/peer complete|done`, `/peer objection|block`, `/peer unblock`, `/peer ping`, `/peer drop`, `/peer pass|fail` — short goal-board aliases",
|
|
140
|
+
"- `/peer goal create <objective> [--constraint <a,b>]` — start a flat shared goal board",
|
|
141
|
+
"- `/peer goal list|show [goal-id]` — inspect peer goals, active claims, blockers, and votes",
|
|
142
|
+
"- `/peer goal fanout <goal-id> <objective> --peer <id[,id]> [--path <a,b>] [--send] [--no-await]` — plan or dispatch role-specific peer lanes",
|
|
143
|
+
"- `/peer goal task|finding|handoff|note <goal-id> <summary> [--path <a,b>] [--status done]` — post goal-board events",
|
|
144
|
+
"- `/peer goal claim <goal-id> <task> --mode write --path <a,b> [--ttl-ms <ms>] [--stale-after-ms <ms>]` — lease work without hierarchy",
|
|
145
|
+
"- `/peer goal heartbeat <goal-id> <claim-event-id> [summary] [--ttl-ms <ms>] [--stale-after-ms <ms>]` — refresh a live or stale claim",
|
|
146
|
+
"- `/peer goal release <goal-id> <claim-event-id> [summary]` — release a claimed lane",
|
|
147
|
+
"- `/peer goal object <goal-id> <reason> [--path <a,b>]`, `/peer goal resolve <goal-id> <event-id> <summary>`, `/peer goal vote <goal-id> <pass|fail|pass-with-risks> [summary]`",
|
|
148
|
+
"- `/peer get <peer|message|conversation|runtime|audit|goals|goal-id>` — inspect peer state",
|
|
149
|
+
"- `/peer await <message-id> [...message-id] [--timeout-ms <ms>]` — wait for queued peer replies",
|
|
150
|
+
"- `/peer help` — show this help",
|
|
151
|
+
].join("\n");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function formatPeerInitResult(result) {
|
|
155
|
+
if (result.created) return `Created ${result.relativePath || ".pi/peers.json"}. Edit it to add trusted peers before sending work.`;
|
|
156
|
+
return `${result.relativePath || ".pi/peers.json"} already exists; left it unchanged.`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function formatPeerCommandError(message) {
|
|
160
|
+
return `${message}\n\nRun \`/peer help\` for usage.`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parsePeerGoalCommand(parsed, flags, positionals) {
|
|
164
|
+
const action = positionals[0] || "list";
|
|
165
|
+
const rest = positionals.slice(1);
|
|
166
|
+
const withAction = { ...parsed, goalAction: action };
|
|
167
|
+
if (action === "list") return withAction;
|
|
168
|
+
if (action === "create") {
|
|
169
|
+
const objective = rest.join(" ").trim();
|
|
170
|
+
if (!objective) return { ...withAction, error: "/peer goal create requires <objective>" };
|
|
171
|
+
return { ...withAction, objective, constraints: listFlag(flags.constraint || flags.constraints) };
|
|
172
|
+
}
|
|
173
|
+
if (action === "show") return { ...withAction, goalId: rest[0] };
|
|
174
|
+
if (action === "fanout") {
|
|
175
|
+
const goalId = rest[0];
|
|
176
|
+
const objective = rest.slice(1).join(" ").trim();
|
|
177
|
+
const peers = listFlag(flags.peer || flags.peers);
|
|
178
|
+
if (!goalId || !objective) return { ...withAction, error: "/peer goal fanout requires <goal-id> <objective> --peer <id[,id]>" };
|
|
179
|
+
if (!peers.length) return { ...withAction, error: "/peer goal fanout requires --peer <id[,id]>" };
|
|
180
|
+
return {
|
|
181
|
+
...withAction,
|
|
182
|
+
goalId,
|
|
183
|
+
objective,
|
|
184
|
+
peers,
|
|
185
|
+
paths: listFlag(flags.path || flags.paths),
|
|
186
|
+
send: flagEnabled(flags.send),
|
|
187
|
+
awaitResponse: !flagEnabled(flags.noAwait) && stringFlag(flags.await, "true") !== "false",
|
|
188
|
+
timeoutMs: positiveIntegerFlag(flags.timeoutMs),
|
|
189
|
+
staleAfterMs: positiveIntegerFlag(flags.staleAfterMs),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
if (["task", "finding", "handoff", "note"].includes(action)) {
|
|
193
|
+
const goalId = rest[0];
|
|
194
|
+
const summary = rest.slice(1).join(" ").trim();
|
|
195
|
+
if (!goalId || !summary) return { ...withAction, error: `/peer goal ${action} requires <goal-id> <summary>` };
|
|
196
|
+
return { ...withAction, goalId, eventType: action === "task" ? "task" : action, summary, paths: listFlag(flags.path || flags.paths), severity: stringFlag(flags.severity, undefined), taskId: stringFlag(flags.taskId, undefined), status: stringFlag(flags.status, undefined) };
|
|
197
|
+
}
|
|
198
|
+
if (action === "claim") {
|
|
199
|
+
if (flagEnabled(flags.write) && flags.mode === undefined) flags.mode = "write";
|
|
200
|
+
const goalId = rest[0];
|
|
201
|
+
const summary = rest.slice(1).join(" ").trim();
|
|
202
|
+
if (!goalId || !summary) return { ...withAction, error: "/peer goal claim requires <goal-id> <task>" };
|
|
203
|
+
return { ...withAction, goalId, summary, paths: listFlag(flags.path || flags.paths), mode: stringFlag(flags.mode, "read"), ttlMs: positiveIntegerFlag(flags.ttlMs), staleAfterMs: positiveIntegerFlag(flags.staleAfterMs) };
|
|
204
|
+
}
|
|
205
|
+
if (action === "heartbeat") {
|
|
206
|
+
const goalId = rest[0];
|
|
207
|
+
const resolves = rest[1];
|
|
208
|
+
const summary = rest.slice(2).join(" ").trim() || `Heartbeat for ${resolves || "claim"}`;
|
|
209
|
+
if (!goalId || !resolves) return { ...withAction, error: "/peer goal heartbeat requires <goal-id> <claim-event-id> [summary]" };
|
|
210
|
+
return { ...withAction, goalId, resolves, summary, ttlMs: positiveIntegerFlag(flags.ttlMs), staleAfterMs: positiveIntegerFlag(flags.staleAfterMs) };
|
|
211
|
+
}
|
|
212
|
+
if (action === "release") {
|
|
213
|
+
const goalId = rest[0];
|
|
214
|
+
const resolves = rest[1];
|
|
215
|
+
const summary = rest.slice(2).join(" ").trim() || `Released ${resolves || "claim"}`;
|
|
216
|
+
if (!goalId || !resolves) return { ...withAction, error: "/peer goal release requires <goal-id> <claim-event-id> [summary]" };
|
|
217
|
+
return { ...withAction, goalId, resolves, summary };
|
|
218
|
+
}
|
|
219
|
+
if (action === "object") {
|
|
220
|
+
const goalId = rest[0];
|
|
221
|
+
const summary = rest.slice(1).join(" ").trim();
|
|
222
|
+
if (!goalId || !summary) return { ...withAction, error: "/peer goal object requires <goal-id> <reason>" };
|
|
223
|
+
return { ...withAction, goalId, summary, paths: listFlag(flags.path || flags.paths), severity: stringFlag(flags.severity, "blocking") };
|
|
224
|
+
}
|
|
225
|
+
if (action === "resolve") {
|
|
226
|
+
const goalId = rest[0];
|
|
227
|
+
const resolves = rest[1];
|
|
228
|
+
const summary = rest.slice(2).join(" ").trim() || `Resolved ${resolves || "objection"}`;
|
|
229
|
+
if (!goalId || !resolves) return { ...withAction, error: "/peer goal resolve requires <goal-id> <event-id> [summary]" };
|
|
230
|
+
return { ...withAction, goalId, resolves, summary };
|
|
231
|
+
}
|
|
232
|
+
if (action === "vote") {
|
|
233
|
+
const goalId = rest[0];
|
|
234
|
+
const verdict = rest[1];
|
|
235
|
+
const summary = rest.slice(2).join(" ").trim();
|
|
236
|
+
if (!goalId || !verdict) return { ...withAction, error: "/peer goal vote requires <goal-id> <pass|fail|pass-with-risks> [summary]" };
|
|
237
|
+
return { ...withAction, goalId, verdict, summary, confidence: flags.confidence };
|
|
238
|
+
}
|
|
239
|
+
if (action === "close") {
|
|
240
|
+
const goalId = rest[0];
|
|
241
|
+
const summary = rest.slice(1).join(" ").trim();
|
|
242
|
+
if (!goalId) return { ...withAction, error: "/peer goal close requires <goal-id>" };
|
|
243
|
+
return { ...withAction, goalId, summary, force: flagEnabled(flags.force) };
|
|
244
|
+
}
|
|
245
|
+
return { ...withAction, error: `Unknown /peer goal action '${action}'` };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function stringFlag(value, fallback) {
|
|
249
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
250
|
+
return fallback;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function positiveIntegerFlag(value) {
|
|
254
|
+
if (value === undefined || value === true) return undefined;
|
|
255
|
+
const number = Number(value);
|
|
256
|
+
return Number.isInteger(number) && number > 0 ? number : undefined;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function metadataFromFlags(flags = {}, options = {}) {
|
|
260
|
+
const claimedPaths = options.claimedPaths || claimedPathsFlag(flags.claim || flags.claimedPath || flags.claimedPaths);
|
|
261
|
+
const goalId = options.goalId || stringFlag(flags.goal || flags.goalId, undefined);
|
|
262
|
+
return {
|
|
263
|
+
...(claimedPaths.length ? { claimedPaths } : {}),
|
|
264
|
+
...(goalId ? { goalId } : {}),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function claimedPathsFlag(value) {
|
|
269
|
+
return listFlag(value);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function stripUndefined(object) {
|
|
273
|
+
return Object.fromEntries(Object.entries(object).filter(([, value]) => value !== undefined));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function capabilitiesFromFlags(flags = {}) {
|
|
277
|
+
const capabilities = {};
|
|
278
|
+
const intents = listFlag(flags.intents);
|
|
279
|
+
if (intents.length) capabilities.intents = intents;
|
|
280
|
+
if (flagEnabled(flags.write) || flagEnabled(flags.writeAccess)) capabilities.writeAccess = true;
|
|
281
|
+
if (flagEnabled(flags.readOnly)) capabilities.writeAccess = false;
|
|
282
|
+
return capabilities;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function listFlag(value) {
|
|
286
|
+
if (Array.isArray(value)) return [...new Set(value.map((item) => String(item).trim()).filter(Boolean))];
|
|
287
|
+
if (typeof value !== "string" || !value.trim()) return [];
|
|
288
|
+
return [...new Set(value.split(",").map((item) => item.trim()).filter(Boolean))];
|
|
289
|
+
}
|