@cleocode/cleo-os 2026.4.12

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CLEO Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CleoOS postinstall — scaffolds global XDG hub and deploys extensions.
4
+ *
5
+ * Runs automatically after `npm install -g @cleocode/cleo-os`.
6
+ * Creates XDG-compliant directory structure and copies the CANT bridge
7
+ * extension template into the extensions directory.
8
+ *
9
+ * Skips during workspace/dev installs (non-global).
10
+ *
11
+ * This file is plain JS (not compiled from src/) so it can run before
12
+ * the package is built, matching the @cleocode/cleo postinstall pattern.
13
+ */
14
+
15
+ import { existsSync, mkdirSync, cpSync } from 'node:fs';
16
+ import { dirname, join, resolve } from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+ import { homedir } from 'node:os';
19
+
20
+ const __filename = fileURLToPath(import.meta.url);
21
+ const __dirname = dirname(__filename);
22
+
23
+ /**
24
+ * Detect if this is a global npm install (not a workspace/dev install).
25
+ */
26
+ function isGlobalInstall() {
27
+ const pkgRoot = resolve(__dirname, '..');
28
+
29
+ // Signal 1: npm_config_global env var (set by npm during global installs)
30
+ if (process.env.npm_config_global === 'true') return true;
31
+
32
+ // Signal 2: path contains a global node_modules (npm, pnpm, yarn)
33
+ if (/[/\\]lib[/\\]node_modules[/\\]/.test(pkgRoot)) return true;
34
+
35
+ // Signal 3: npm_config_prefix matches the package path
36
+ const prefix = process.env.npm_config_prefix;
37
+ if (prefix && pkgRoot.startsWith(prefix)) return true;
38
+
39
+ // Signal 4: inside a pnpm workspace — definitely not global
40
+ const workspaceMarker = join(pkgRoot, '..', '..', 'pnpm-workspace.yaml');
41
+ if (existsSync(workspaceMarker)) return false;
42
+
43
+ return false;
44
+ }
45
+
46
+ /**
47
+ * Inline XDG path resolution (avoids importing from dist/ which may not exist).
48
+ */
49
+ function resolveCleoOsPaths() {
50
+ const home = homedir();
51
+ const xdgData = process.env.XDG_DATA_HOME ?? join(home, '.local', 'share');
52
+ const xdgConfig = process.env.XDG_CONFIG_HOME ?? join(home, '.config');
53
+
54
+ const data = join(xdgData, 'cleo');
55
+ const config = join(xdgConfig, 'cleo');
56
+
57
+ return {
58
+ data,
59
+ config,
60
+ agentDir: data,
61
+ extensions: join(data, 'extensions'),
62
+ cant: join(data, 'cant'),
63
+ auth: join(config, 'auth'),
64
+ };
65
+ }
66
+
67
+ function main() {
68
+ if (!isGlobalInstall()) {
69
+ console.log('CleoOS: skipping postinstall (not global install)');
70
+ return;
71
+ }
72
+
73
+ const paths = resolveCleoOsPaths();
74
+
75
+ // Scaffold directories
76
+ for (const dir of [paths.data, paths.config, paths.extensions, paths.cant, paths.auth]) {
77
+ if (!existsSync(dir)) {
78
+ mkdirSync(dir, { recursive: true });
79
+ console.log(`CleoOS: created ${dir}`);
80
+ }
81
+ }
82
+
83
+ // Deploy bridge extension from package template
84
+ const bridgeTemplate = join(__dirname, '..', 'extensions', 'cleo-cant-bridge.js');
85
+ const bridgeTarget = join(paths.extensions, 'cleo-cant-bridge.js');
86
+ if (existsSync(bridgeTemplate)) {
87
+ cpSync(bridgeTemplate, bridgeTarget, { force: true });
88
+ console.log(`CleoOS: deployed bridge extension to ${bridgeTarget}`);
89
+ }
90
+
91
+ console.log('CleoOS: postinstall complete');
92
+ }
93
+
94
+ main();
File without changes
@@ -0,0 +1,371 @@
1
+ /**
2
+ * CleoOS chat room — inter-agent messaging TUI.
3
+ *
4
+ * Installed to: $CLEO_HOME/pi-extensions/cleo-chatroom.ts
5
+ * Loaded by: Pi via `-e <path>` or settings.json extensions array
6
+ *
7
+ * Wave 7 — surfaces inter-agent traffic as a TUI panel per ULTRAPLAN
8
+ * section 13. Agents communicate through four tools:
9
+ *
10
+ * - `send_to_lead` — worker sends a message to their lead.
11
+ * - `broadcast_to_team` — lead broadcasts to all group workers.
12
+ * - `report_to_orchestrator` — lead reports status to the orchestrator.
13
+ * - `query_peer` — worker queries another worker in the same group.
14
+ *
15
+ * Each tool appends a structured JSONL entry to the Pi session's message
16
+ * log. A TUI widget (registered on `session_start`) renders the last N
17
+ * messages in a scrollable panel below the editor.
18
+ *
19
+ * This is a TEMPLATE extension — it uses `import type` for Pi types and
20
+ * mirrors the patterns established by the existing extensions in
21
+ * `packages/cleo/templates/cleoos-hub/pi-extensions/`.
22
+ *
23
+ * @packageDocumentation
24
+ */
25
+
26
+ import { appendFileSync, mkdirSync } from "node:fs";
27
+ import { join } from "node:path";
28
+ import type {
29
+ ExtensionAPI,
30
+ ExtensionCommandContext,
31
+ ExtensionContext,
32
+ } from "@mariozechner/pi-coding-agent";
33
+ import { Type } from "@sinclair/typebox";
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Message model
37
+ // ---------------------------------------------------------------------------
38
+
39
+ /** A single inter-agent chat message. */
40
+ interface ChatMessage {
41
+ /** ISO-8601 timestamp of when the message was created. */
42
+ timestamp: string;
43
+ /** Name of the sending agent. */
44
+ from: string;
45
+ /** Name of the receiving agent or group (e.g. "team:backend"). */
46
+ to: string;
47
+ /** Message channel identifying the tool that produced this message. */
48
+ channel: "send_to_lead" | "broadcast_to_team" | "report_to_orchestrator" | "query_peer";
49
+ /** The message text. */
50
+ text: string;
51
+ }
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Module state
55
+ // ---------------------------------------------------------------------------
56
+
57
+ const WIDGET_KEY = "cleo-chatroom";
58
+ const STATUS_KEY = "cleo-chatroom";
59
+ const MAX_DISPLAY_MESSAGES = 15;
60
+
61
+ /** In-memory message buffer for the current session. */
62
+ const messages: ChatMessage[] = [];
63
+
64
+ /** Path to the JSONL log file (set on session_start). */
65
+ let logPath: string | null = null;
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Helpers
69
+ // ---------------------------------------------------------------------------
70
+
71
+ /**
72
+ * Record a chat message: push to the in-memory buffer and append to the
73
+ * JSONL log file. The log file is created lazily on first write.
74
+ *
75
+ * @param msg - The chat message to record.
76
+ */
77
+ function recordMessage(msg: ChatMessage): void {
78
+ messages.push(msg);
79
+ if (logPath) {
80
+ try {
81
+ appendFileSync(logPath, JSON.stringify(msg) + "\n", "utf-8");
82
+ } catch {
83
+ // Best-effort: never crash Pi over a log write failure.
84
+ }
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Format a chat message for TUI display.
90
+ *
91
+ * @param msg - The message to format.
92
+ * @returns A single-line string representation.
93
+ */
94
+ function formatMessage(msg: ChatMessage): string {
95
+ const time = msg.timestamp.slice(11, 19);
96
+ return `[${time}] ${msg.from} -> ${msg.to}: ${msg.text}`;
97
+ }
98
+
99
+ /**
100
+ * Render the chat room widget with the latest messages.
101
+ *
102
+ * @param ctx - The Pi extension context.
103
+ */
104
+ function renderWidget(ctx: ExtensionContext): void {
105
+ if (!ctx.hasUI) return;
106
+ const tail = messages.slice(-MAX_DISPLAY_MESSAGES);
107
+ if (tail.length === 0) {
108
+ ctx.ui.setWidget(WIDGET_KEY, ["(no messages yet)"], {
109
+ placement: "belowEditor",
110
+ });
111
+ return;
112
+ }
113
+ const lines = tail.map(formatMessage);
114
+ ctx.ui.setWidget(WIDGET_KEY, lines, { placement: "belowEditor" });
115
+ ctx.ui.setStatus(STATUS_KEY, `Chat: ${messages.length} msg(s)`);
116
+ }
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // Tool parameter schemas (TypeBox)
120
+ // ---------------------------------------------------------------------------
121
+
122
+ const SendToLeadParams = Type.Object({
123
+ message: Type.String({ description: "Message to send to your team lead" }),
124
+ from: Type.String({ description: "Your agent name" }),
125
+ lead: Type.String({ description: "Name of the lead agent" }),
126
+ });
127
+
128
+ const BroadcastToTeamParams = Type.Object({
129
+ message: Type.String({ description: "Message to broadcast to the team" }),
130
+ from: Type.String({ description: "Your agent name (lead)" }),
131
+ group: Type.String({ description: "Team group name (e.g. 'backend')" }),
132
+ });
133
+
134
+ const ReportToOrchestratorParams = Type.Object({
135
+ message: Type.String({ description: "Status report for the orchestrator" }),
136
+ from: Type.String({ description: "Your agent name (lead)" }),
137
+ orchestrator: Type.String({
138
+ description: "Name of the orchestrator agent",
139
+ }),
140
+ });
141
+
142
+ const QueryPeerParams = Type.Object({
143
+ message: Type.String({ description: "Query for your peer worker" }),
144
+ from: Type.String({ description: "Your agent name" }),
145
+ peer: Type.String({ description: "Name of the peer worker to query" }),
146
+ });
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // Pi extension factory
150
+ // ---------------------------------------------------------------------------
151
+
152
+ /**
153
+ * Pi extension factory for the CleoOS chat room.
154
+ *
155
+ * Registers four inter-agent communication tools and a TUI widget that
156
+ * displays the message stream. Also registers `/cleo:chat-info` for
157
+ * introspection and clears state on session shutdown.
158
+ *
159
+ * @param pi - The Pi extension API instance.
160
+ */
161
+ export default function (pi: ExtensionAPI): void {
162
+ // -------------------------------------------------------------------------
163
+ // session_start: initialize log directory and widget
164
+ // -------------------------------------------------------------------------
165
+ pi.on("session_start", async (_event: unknown, ctx: ExtensionContext) => {
166
+ messages.length = 0;
167
+ logPath = null;
168
+
169
+ try {
170
+ const chatDir = join(ctx.cwd, ".cleo", "chat");
171
+ mkdirSync(chatDir, { recursive: true });
172
+ const sessionTs = new Date().toISOString().replace(/[:.]/g, "-");
173
+ logPath = join(chatDir, `chatroom-${sessionTs}.jsonl`);
174
+ } catch {
175
+ // Best-effort: widget still works without persistent logging.
176
+ }
177
+
178
+ renderWidget(ctx);
179
+
180
+ if (ctx.hasUI) {
181
+ ctx.ui.setStatus(STATUS_KEY, "Chat: ready");
182
+ }
183
+ });
184
+
185
+ // -------------------------------------------------------------------------
186
+ // Tools: send_to_lead
187
+ // -------------------------------------------------------------------------
188
+ pi.registerTool({
189
+ name: "send_to_lead",
190
+ label: "Send to Lead",
191
+ description:
192
+ "Send a message from a worker agent to their team lead. " +
193
+ "Used for status updates, questions, and escalations.",
194
+ parameters: SendToLeadParams,
195
+ async execute(
196
+ _id: string,
197
+ params: { message: string; from: string; lead: string },
198
+ _signal: AbortSignal,
199
+ _onUpdate: (text: string) => void,
200
+ ctx: ExtensionContext,
201
+ ) {
202
+ const msg: ChatMessage = {
203
+ timestamp: new Date().toISOString(),
204
+ from: params.from,
205
+ to: params.lead,
206
+ channel: "send_to_lead",
207
+ text: params.message,
208
+ };
209
+ recordMessage(msg);
210
+ renderWidget(ctx);
211
+ return {
212
+ content: [
213
+ {
214
+ type: "text" as const,
215
+ text: `Message sent to lead ${params.lead}: ${params.message}`,
216
+ },
217
+ ],
218
+ };
219
+ },
220
+ });
221
+
222
+ // -------------------------------------------------------------------------
223
+ // Tools: broadcast_to_team
224
+ // -------------------------------------------------------------------------
225
+ pi.registerTool({
226
+ name: "broadcast_to_team",
227
+ label: "Broadcast to Team",
228
+ description:
229
+ "Broadcast a message from a lead to all workers in their group. " +
230
+ "Used for coordination directives and status announcements.",
231
+ parameters: BroadcastToTeamParams,
232
+ async execute(
233
+ _id: string,
234
+ params: { message: string; from: string; group: string },
235
+ _signal: AbortSignal,
236
+ _onUpdate: (text: string) => void,
237
+ ctx: ExtensionContext,
238
+ ) {
239
+ const msg: ChatMessage = {
240
+ timestamp: new Date().toISOString(),
241
+ from: params.from,
242
+ to: `team:${params.group}`,
243
+ channel: "broadcast_to_team",
244
+ text: params.message,
245
+ };
246
+ recordMessage(msg);
247
+ renderWidget(ctx);
248
+ return {
249
+ content: [
250
+ {
251
+ type: "text" as const,
252
+ text: `Broadcast to team:${params.group}: ${params.message}`,
253
+ },
254
+ ],
255
+ };
256
+ },
257
+ });
258
+
259
+ // -------------------------------------------------------------------------
260
+ // Tools: report_to_orchestrator
261
+ // -------------------------------------------------------------------------
262
+ pi.registerTool({
263
+ name: "report_to_orchestrator",
264
+ label: "Report to Orchestrator",
265
+ description:
266
+ "Send a status report from a lead to the orchestrator. " +
267
+ "Used for task completion reports, blockers, and escalations.",
268
+ parameters: ReportToOrchestratorParams,
269
+ async execute(
270
+ _id: string,
271
+ params: { message: string; from: string; orchestrator: string },
272
+ _signal: AbortSignal,
273
+ _onUpdate: (text: string) => void,
274
+ ctx: ExtensionContext,
275
+ ) {
276
+ const msg: ChatMessage = {
277
+ timestamp: new Date().toISOString(),
278
+ from: params.from,
279
+ to: params.orchestrator,
280
+ channel: "report_to_orchestrator",
281
+ text: params.message,
282
+ };
283
+ recordMessage(msg);
284
+ renderWidget(ctx);
285
+ return {
286
+ content: [
287
+ {
288
+ type: "text" as const,
289
+ text: `Report sent to orchestrator ${params.orchestrator}: ${params.message}`,
290
+ },
291
+ ],
292
+ };
293
+ },
294
+ });
295
+
296
+ // -------------------------------------------------------------------------
297
+ // Tools: query_peer
298
+ // -------------------------------------------------------------------------
299
+ pi.registerTool({
300
+ name: "query_peer",
301
+ label: "Query Peer",
302
+ description:
303
+ "Send a query from one worker to a peer worker in the same group. " +
304
+ "Used for cross-agent information sharing and coordination.",
305
+ parameters: QueryPeerParams,
306
+ async execute(
307
+ _id: string,
308
+ params: { message: string; from: string; peer: string },
309
+ _signal: AbortSignal,
310
+ _onUpdate: (text: string) => void,
311
+ ctx: ExtensionContext,
312
+ ) {
313
+ const msg: ChatMessage = {
314
+ timestamp: new Date().toISOString(),
315
+ from: params.from,
316
+ to: params.peer,
317
+ channel: "query_peer",
318
+ text: params.message,
319
+ };
320
+ recordMessage(msg);
321
+ renderWidget(ctx);
322
+ return {
323
+ content: [
324
+ {
325
+ type: "text" as const,
326
+ text: `Query sent to peer ${params.peer}: ${params.message}`,
327
+ },
328
+ ],
329
+ };
330
+ },
331
+ });
332
+
333
+ // -------------------------------------------------------------------------
334
+ // Command: /cleo:chat-info — introspection
335
+ // -------------------------------------------------------------------------
336
+ pi.registerCommand("cleo:chat-info", {
337
+ description: "Show chat room status and recent messages",
338
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
339
+ const tail = messages.slice(-5);
340
+ const lines = [
341
+ `Chat Room: ${messages.length} total message(s)`,
342
+ `Log: ${logPath ?? "(none)"}`,
343
+ "",
344
+ "Recent messages:",
345
+ ...tail.map(formatMessage),
346
+ ];
347
+ if (tail.length === 0) {
348
+ lines.push(" (no messages yet)");
349
+ }
350
+ pi.sendMessage(
351
+ {
352
+ customType: "cleo-chatroom-info",
353
+ content: lines.join("\n"),
354
+ display: true,
355
+ },
356
+ { triggerTurn: false },
357
+ );
358
+ if (ctx.hasUI) {
359
+ ctx.ui.notify(`Chat room: ${messages.length} messages`, "info");
360
+ }
361
+ },
362
+ });
363
+
364
+ // -------------------------------------------------------------------------
365
+ // session_shutdown: clear state
366
+ // -------------------------------------------------------------------------
367
+ pi.on("session_shutdown", async () => {
368
+ messages.length = 0;
369
+ logPath = null;
370
+ });
371
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@cleocode/cleo-os",
3
+ "version": "2026.4.12",
4
+ "description": "CleoOS — the batteries-included agentic development environment wrapping Pi",
5
+ "type": "module",
6
+ "main": "./dist/cli.js",
7
+ "bin": {
8
+ "cleoos": "dist/cli.js"
9
+ },
10
+ "dependencies": {
11
+ "@cleocode/cleo": "2026.4.12",
12
+ "@cleocode/cant": "2026.4.12"
13
+ },
14
+ "peerDependencies": {
15
+ "@mariozechner/pi-coding-agent": ">=0.60.0"
16
+ },
17
+ "peerDependenciesMeta": {
18
+ "@mariozechner/pi-coding-agent": {
19
+ "optional": true
20
+ }
21
+ },
22
+ "devDependencies": {
23
+ "typescript": "^5.9.0",
24
+ "vitest": "^4.1.0"
25
+ },
26
+ "engines": {
27
+ "node": ">=24.0.0"
28
+ },
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/kryptobaseddev/cleo",
33
+ "directory": "packages/cleo-os"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "files": [
39
+ "dist",
40
+ "extensions",
41
+ "bin"
42
+ ],
43
+ "scripts": {
44
+ "build": "tsc",
45
+ "typecheck": "tsc --noEmit",
46
+ "test": "vitest run",
47
+ "postinstall": "node bin/postinstall.js"
48
+ }
49
+ }