@cleocode/cleo-os 2026.4.12 → 2026.4.16
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/cli.d.ts +14 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +77 -0
- package/dist/cli.js.map +1 -0
- package/dist/keystore.d.ts +27 -0
- package/dist/keystore.d.ts.map +1 -0
- package/dist/keystore.js +35 -0
- package/dist/keystore.js.map +1 -0
- package/dist/postinstall.d.ts +23 -0
- package/dist/postinstall.d.ts.map +1 -0
- package/dist/postinstall.js +208 -0
- package/dist/postinstall.js.map +1 -0
- package/dist/xdg.d.ts +38 -0
- package/dist/xdg.d.ts.map +1 -0
- package/dist/xdg.js +39 -0
- package/dist/xdg.js.map +1 -0
- package/extensions/cleo-cant-bridge.ts +598 -0
- package/extensions/cleo-chatroom.ts +78 -7
- package/package.json +10 -7
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CleoOS launcher — the batteries-included agentic development environment.
|
|
4
|
+
*
|
|
5
|
+
* Wraps Pi's `main()` entry point with the cleo-cant-bridge pre-loaded
|
|
6
|
+
* as an extension. Pi stays upstream (ULTRAPLAN L1). This is a thin
|
|
7
|
+
* launcher that injects CleoOS extensions into Pi's CLI argument list.
|
|
8
|
+
*
|
|
9
|
+
* Usage: `cleoos [pi-args...]` — launches Pi with CANT bridge extension.
|
|
10
|
+
*
|
|
11
|
+
* @packageDocumentation
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;GAUG"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CleoOS launcher — the batteries-included agentic development environment.
|
|
4
|
+
*
|
|
5
|
+
* Wraps Pi's `main()` entry point with the cleo-cant-bridge pre-loaded
|
|
6
|
+
* as an extension. Pi stays upstream (ULTRAPLAN L1). This is a thin
|
|
7
|
+
* launcher that injects CleoOS extensions into Pi's CLI argument list.
|
|
8
|
+
*
|
|
9
|
+
* Usage: `cleoos [pi-args...]` — launches Pi with CANT bridge extension.
|
|
10
|
+
*
|
|
11
|
+
* @packageDocumentation
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { resolveCleoOsPaths } from './xdg.js';
|
|
16
|
+
/**
|
|
17
|
+
* Collect CleoOS extension paths that exist on disk.
|
|
18
|
+
*
|
|
19
|
+
* Resolves the CANT bridge extension from the XDG data directory.
|
|
20
|
+
* Only returns paths for extensions that actually exist on the filesystem.
|
|
21
|
+
*
|
|
22
|
+
* @returns Array of absolute extension file paths.
|
|
23
|
+
*/
|
|
24
|
+
function collectExtensionPaths() {
|
|
25
|
+
const paths = resolveCleoOsPaths();
|
|
26
|
+
const extensions = [];
|
|
27
|
+
const bridgePath = join(paths.extensions, 'cleo-cant-bridge.js');
|
|
28
|
+
if (existsSync(bridgePath)) {
|
|
29
|
+
extensions.push(bridgePath);
|
|
30
|
+
}
|
|
31
|
+
return extensions;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Build the argument list for Pi's `main()`, injecting CleoOS extensions.
|
|
35
|
+
*
|
|
36
|
+
* Takes the user's CLI arguments (everything after `cleoos`) and prepends
|
|
37
|
+
* `--extension <path>` flags for each discovered CleoOS extension.
|
|
38
|
+
*
|
|
39
|
+
* @param userArgs - Arguments passed to `cleoos` by the user.
|
|
40
|
+
* @param extensionPaths - Resolved extension paths to inject.
|
|
41
|
+
* @returns Combined argument array for Pi's `main()`.
|
|
42
|
+
*/
|
|
43
|
+
function buildArgs(userArgs, extensionPaths) {
|
|
44
|
+
const extensionFlags = extensionPaths.flatMap((p) => ['--extension', p]);
|
|
45
|
+
return [...extensionFlags, ...userArgs];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Entry point for the `cleoos` binary.
|
|
49
|
+
*
|
|
50
|
+
* Dynamically imports Pi's coding agent (peerDependency), resolves CleoOS
|
|
51
|
+
* extension paths, and delegates to Pi's `main()` with the bridge extension
|
|
52
|
+
* injected into the argument list.
|
|
53
|
+
*
|
|
54
|
+
* Exits with code 1 if Pi is not installed, providing install instructions.
|
|
55
|
+
*/
|
|
56
|
+
async function main() {
|
|
57
|
+
// Dynamically import Pi — it's a peerDependency, may not be installed
|
|
58
|
+
let piMain;
|
|
59
|
+
try {
|
|
60
|
+
const pi = await import('@mariozechner/pi-coding-agent');
|
|
61
|
+
piMain = pi.main;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
console.error('CleoOS requires Pi Coding Agent to be installed.\n' +
|
|
65
|
+
'Run: npm install -g @mariozechner/pi-coding-agent\n' +
|
|
66
|
+
'Then try again: cleoos');
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
const extensionPaths = collectExtensionPaths();
|
|
70
|
+
const args = buildArgs(process.argv.slice(2), extensionPaths);
|
|
71
|
+
await piMain(args);
|
|
72
|
+
}
|
|
73
|
+
main().catch((err) => {
|
|
74
|
+
console.error('CleoOS fatal:', err instanceof Error ? err.message : String(err));
|
|
75
|
+
process.exit(1);
|
|
76
|
+
});
|
|
77
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAE9C;;;;;;;GAOG;AACH,SAAS,qBAAqB;IAC5B,MAAM,KAAK,GAAG,kBAAkB,EAAE,CAAC;IACnC,MAAM,UAAU,GAAa,EAAE,CAAC;IAEhC,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,qBAAqB,CAAC,CAAC;IACjE,IAAI,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC3B,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAC9B,CAAC;IAED,OAAO,UAAU,CAAC;AACpB,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,SAAS,CAAC,QAAkB,EAAE,cAAwB;IAC7D,MAAM,cAAc,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,CAAC;IACzE,OAAO,CAAC,GAAG,cAAc,EAAE,GAAG,QAAQ,CAAC,CAAC;AAC1C,CAAC;AAED;;;;;;;;GAQG;AACH,KAAK,UAAU,IAAI;IACjB,sEAAsE;IACtE,IAAI,MAAyC,CAAC;IAC9C,IAAI,CAAC;QACH,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,+BAA+B,CAAC,CAAC;QACzD,MAAM,GAAG,EAAE,CAAC,IAAI,CAAC;IACnB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,KAAK,CACX,oDAAoD;YAClD,qDAAqD;YACrD,wBAAwB,CAC3B,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,cAAc,GAAG,qBAAqB,EAAE,CAAC;IAC/C,MAAM,IAAI,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC;IAE9D,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC;AACrB,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IAC5B,OAAO,CAAC,KAAK,CAAC,eAAe,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IACjF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CleoOS keystore — Pi API key management with XDG-compliant storage.
|
|
3
|
+
*
|
|
4
|
+
* Wraps Pi's `FileAuthStorageBackend` with a CleoOS-specific XDG path so
|
|
5
|
+
* that credentials are persisted at `~/.config/cleo/auth/auth.json` (the
|
|
6
|
+
* `auth` sub-path of the CleoOS XDG config directory) rather than Pi's
|
|
7
|
+
* default location.
|
|
8
|
+
*
|
|
9
|
+
* This ensures:
|
|
10
|
+
* - Credentials survive `cleoos` reinstalls (XDG is outside the package).
|
|
11
|
+
* - Multiple CleoOS installations on the same host share credentials.
|
|
12
|
+
* - The auth file lives under the XDG Config spec (`~/.config/cleo/`).
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
import { FileAuthStorageBackend } from '@mariozechner/pi-coding-agent';
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a `FileAuthStorageBackend` configured to use the CleoOS XDG
|
|
19
|
+
* auth directory (`~/.config/cleo/auth/auth.json`).
|
|
20
|
+
*
|
|
21
|
+
* The directory is NOT created here — that is handled by the postinstall
|
|
22
|
+
* script so that directory creation is a one-time operation.
|
|
23
|
+
*
|
|
24
|
+
* @returns A `FileAuthStorageBackend` pointed at the XDG-compliant path.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveKeystore(): FileAuthStorageBackend;
|
|
27
|
+
//# sourceMappingURL=keystore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keystore.d.ts","sourceRoot":"","sources":["../src/keystore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAMvE;;;;;;;;GAQG;AACH,wBAAgB,eAAe,IAAI,sBAAsB,CAIxD"}
|
package/dist/keystore.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CleoOS keystore — Pi API key management with XDG-compliant storage.
|
|
3
|
+
*
|
|
4
|
+
* Wraps Pi's `FileAuthStorageBackend` with a CleoOS-specific XDG path so
|
|
5
|
+
* that credentials are persisted at `~/.config/cleo/auth/auth.json` (the
|
|
6
|
+
* `auth` sub-path of the CleoOS XDG config directory) rather than Pi's
|
|
7
|
+
* default location.
|
|
8
|
+
*
|
|
9
|
+
* This ensures:
|
|
10
|
+
* - Credentials survive `cleoos` reinstalls (XDG is outside the package).
|
|
11
|
+
* - Multiple CleoOS installations on the same host share credentials.
|
|
12
|
+
* - The auth file lives under the XDG Config spec (`~/.config/cleo/`).
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { FileAuthStorageBackend } from '@mariozechner/pi-coding-agent';
|
|
18
|
+
import { resolveCleoOsPaths } from './xdg.js';
|
|
19
|
+
/** Default auth file name inside the keystore directory. */
|
|
20
|
+
const AUTH_FILENAME = 'auth.json';
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a `FileAuthStorageBackend` configured to use the CleoOS XDG
|
|
23
|
+
* auth directory (`~/.config/cleo/auth/auth.json`).
|
|
24
|
+
*
|
|
25
|
+
* The directory is NOT created here — that is handled by the postinstall
|
|
26
|
+
* script so that directory creation is a one-time operation.
|
|
27
|
+
*
|
|
28
|
+
* @returns A `FileAuthStorageBackend` pointed at the XDG-compliant path.
|
|
29
|
+
*/
|
|
30
|
+
export function resolveKeystore() {
|
|
31
|
+
const paths = resolveCleoOsPaths();
|
|
32
|
+
const authFilePath = join(paths.auth, AUTH_FILENAME);
|
|
33
|
+
return new FileAuthStorageBackend(authFilePath);
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=keystore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keystore.js","sourceRoot":"","sources":["../src/keystore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAE9C,4DAA4D;AAC5D,MAAM,aAAa,GAAG,WAAW,CAAC;AAElC;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe;IAC7B,MAAM,KAAK,GAAG,kBAAkB,EAAE,CAAC;IACnC,MAAM,YAAY,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;IACrD,OAAO,IAAI,sBAAsB,CAAC,YAAY,CAAC,CAAC;AAClD,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CleoOS postinstall — scaffolds the global XDG hub and deploys extensions.
|
|
3
|
+
*
|
|
4
|
+
* Runs automatically after `npm install -g @cleocode/cleo-os`.
|
|
5
|
+
* Creates an XDG-compliant directory structure, copies the compiled CANT
|
|
6
|
+
* bridge extension to the extensions directory, and optionally invokes
|
|
7
|
+
* `cleo skills install` for any bundled CleoOS skills.
|
|
8
|
+
*
|
|
9
|
+
* Behaviour:
|
|
10
|
+
* - Skips silently during workspace/dev installs (non-global).
|
|
11
|
+
* - All directory creation is idempotent (no-op if directory exists).
|
|
12
|
+
* - All file copies are idempotent (only copies if target is missing).
|
|
13
|
+
* - Skill install is best-effort; failures are logged but not fatal.
|
|
14
|
+
* - Missing `@mariozechner/pi-coding-agent` is handled gracefully.
|
|
15
|
+
*
|
|
16
|
+
* This source compiles to `bin/postinstall.js` via a dedicated tsconfig
|
|
17
|
+
* (see `tsconfig.postinstall.json`). The `postinstall` script in
|
|
18
|
+
* `package.json` references the compiled output at `bin/postinstall.js`.
|
|
19
|
+
*
|
|
20
|
+
* @packageDocumentation
|
|
21
|
+
*/
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=postinstall.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postinstall.d.ts","sourceRoot":"","sources":["../src/postinstall.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG"}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CleoOS postinstall — scaffolds the global XDG hub and deploys extensions.
|
|
3
|
+
*
|
|
4
|
+
* Runs automatically after `npm install -g @cleocode/cleo-os`.
|
|
5
|
+
* Creates an XDG-compliant directory structure, copies the compiled CANT
|
|
6
|
+
* bridge extension to the extensions directory, and optionally invokes
|
|
7
|
+
* `cleo skills install` for any bundled CleoOS skills.
|
|
8
|
+
*
|
|
9
|
+
* Behaviour:
|
|
10
|
+
* - Skips silently during workspace/dev installs (non-global).
|
|
11
|
+
* - All directory creation is idempotent (no-op if directory exists).
|
|
12
|
+
* - All file copies are idempotent (only copies if target is missing).
|
|
13
|
+
* - Skill install is best-effort; failures are logged but not fatal.
|
|
14
|
+
* - Missing `@mariozechner/pi-coding-agent` is handled gracefully.
|
|
15
|
+
*
|
|
16
|
+
* This source compiles to `bin/postinstall.js` via a dedicated tsconfig
|
|
17
|
+
* (see `tsconfig.postinstall.json`). The `postinstall` script in
|
|
18
|
+
* `package.json` references the compiled output at `bin/postinstall.js`.
|
|
19
|
+
*
|
|
20
|
+
* @packageDocumentation
|
|
21
|
+
*/
|
|
22
|
+
import { execFileSync } from 'node:child_process';
|
|
23
|
+
import { cpSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
24
|
+
import { homedir } from 'node:os';
|
|
25
|
+
import { dirname, join, resolve } from 'node:path';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
28
|
+
const __dirname = dirname(__filename);
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// XDG path resolution (inline copy — avoids importing from dist/ which may
|
|
31
|
+
// not exist when this script runs for the first time)
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
/**
|
|
34
|
+
* Inline XDG path resolution that mirrors `src/xdg.ts`.
|
|
35
|
+
*
|
|
36
|
+
* Uses an inline copy here so the postinstall script can run before
|
|
37
|
+
* the compiled `dist/` tree is available on a fresh install.
|
|
38
|
+
*
|
|
39
|
+
* @returns Resolved CleoOS XDG directory paths.
|
|
40
|
+
*/
|
|
41
|
+
function resolveCleoOsPaths() {
|
|
42
|
+
const home = homedir();
|
|
43
|
+
const xdgData = process.env['XDG_DATA_HOME'] ?? join(home, '.local', 'share');
|
|
44
|
+
const xdgConfig = process.env['XDG_CONFIG_HOME'] ?? join(home, '.config');
|
|
45
|
+
const data = join(xdgData, 'cleo');
|
|
46
|
+
const config = join(xdgConfig, 'cleo');
|
|
47
|
+
return {
|
|
48
|
+
data,
|
|
49
|
+
config,
|
|
50
|
+
agentDir: data,
|
|
51
|
+
extensions: join(data, 'extensions'),
|
|
52
|
+
cant: join(data, 'cant'),
|
|
53
|
+
auth: join(config, 'auth'),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Global install detection
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
/**
|
|
60
|
+
* Detect whether this is a global npm / pnpm install.
|
|
61
|
+
*
|
|
62
|
+
* Uses four heuristics in priority order:
|
|
63
|
+
* 1. `npm_config_global=true` env var (set by npm/pnpm for global installs)
|
|
64
|
+
* 2. Package path contains `lib/node_modules/` (npm global pattern)
|
|
65
|
+
* 3. Package path starts with `npm_config_prefix` (npm prefix-based check)
|
|
66
|
+
* 4. Presence of `pnpm-workspace.yaml` two levels up (workspace = dev)
|
|
67
|
+
*
|
|
68
|
+
* @returns `true` if the install appears to be a global install.
|
|
69
|
+
*/
|
|
70
|
+
function isGlobalInstall() {
|
|
71
|
+
const pkgRoot = resolve(__dirname, '..');
|
|
72
|
+
// Signal 1: npm_config_global env var (set by npm during global installs)
|
|
73
|
+
if (process.env['npm_config_global'] === 'true')
|
|
74
|
+
return true;
|
|
75
|
+
// Signal 2: path contains a global node_modules (npm, pnpm, yarn)
|
|
76
|
+
if (/[/\\]lib[/\\]node_modules[/\\]/.test(pkgRoot))
|
|
77
|
+
return true;
|
|
78
|
+
// Signal 3: npm_config_prefix matches the package path
|
|
79
|
+
const prefix = process.env['npm_config_prefix'];
|
|
80
|
+
if (prefix !== undefined && pkgRoot.startsWith(prefix))
|
|
81
|
+
return true;
|
|
82
|
+
// Signal 4: inside a pnpm workspace — definitely not global
|
|
83
|
+
const workspaceMarker = join(pkgRoot, '..', '..', 'pnpm-workspace.yaml');
|
|
84
|
+
if (existsSync(workspaceMarker))
|
|
85
|
+
return false;
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Directory scaffolding
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
/**
|
|
92
|
+
* Idempotently create a directory if it does not already exist.
|
|
93
|
+
*
|
|
94
|
+
* @param dir - Absolute path to the directory to create.
|
|
95
|
+
*/
|
|
96
|
+
function ensureDir(dir) {
|
|
97
|
+
if (!existsSync(dir)) {
|
|
98
|
+
mkdirSync(dir, { recursive: true });
|
|
99
|
+
process.stdout.write(`CleoOS: created ${dir}\n`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Extension deployment
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
/**
|
|
106
|
+
* Copy a compiled extension to the XDG extensions directory.
|
|
107
|
+
*
|
|
108
|
+
* Only copies if the target does not already exist (idempotent). The
|
|
109
|
+
* source is the compiled `.js` file in the package's `extensions/` folder.
|
|
110
|
+
*
|
|
111
|
+
* @param extensionName - Filename without the `.js` extension.
|
|
112
|
+
* @param pkgRoot - Absolute path to the installed package root.
|
|
113
|
+
* @param extensionsDir - Absolute path to the XDG extensions directory.
|
|
114
|
+
*/
|
|
115
|
+
function deployExtension(extensionName, pkgRoot, extensionsDir) {
|
|
116
|
+
const src = join(pkgRoot, 'extensions', `${extensionName}.js`);
|
|
117
|
+
const dest = join(extensionsDir, `${extensionName}.js`);
|
|
118
|
+
if (!existsSync(src)) {
|
|
119
|
+
process.stdout.write(`CleoOS: skipping ${extensionName}.js (source not found at ${src})\n`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (existsSync(dest)) {
|
|
123
|
+
// Already deployed — idempotent, skip.
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
cpSync(src, dest, { force: false });
|
|
127
|
+
process.stdout.write(`CleoOS: deployed ${extensionName}.js to ${dest}\n`);
|
|
128
|
+
}
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// Default CANT file scaffolding
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
/**
|
|
133
|
+
* Write a default `model-routing.cant` stub to the XDG CANT directory if
|
|
134
|
+
* no `.cant` files are present. This gives the user a starting point for
|
|
135
|
+
* CANT declarations without overwriting any existing work.
|
|
136
|
+
*
|
|
137
|
+
* @param cantDir - Absolute path to the XDG CANT directory.
|
|
138
|
+
*/
|
|
139
|
+
function scaffoldDefaultCant(cantDir) {
|
|
140
|
+
const modelRoutingPath = join(cantDir, 'model-routing.cant');
|
|
141
|
+
if (existsSync(modelRoutingPath))
|
|
142
|
+
return;
|
|
143
|
+
const stub = [
|
|
144
|
+
'# CleoOS default model-routing.cant',
|
|
145
|
+
'# Declare agents, teams, and routing rules here.',
|
|
146
|
+
'# See: https://github.com/kryptobaseddev/cleo/blob/main/docs/cant-dsl.md',
|
|
147
|
+
'',
|
|
148
|
+
].join('\n');
|
|
149
|
+
try {
|
|
150
|
+
writeFileSync(modelRoutingPath, stub, 'utf-8');
|
|
151
|
+
process.stdout.write(`CleoOS: created default ${modelRoutingPath}\n`);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// Best-effort: non-fatal.
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// Skill installation
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
/**
|
|
161
|
+
* Invoke `cleo skills install` via `execFileSync` to register the CleoOS
|
|
162
|
+
* bundled skills with the project. This is best-effort — if `cleo` is not
|
|
163
|
+
* on PATH or the command fails, we log and continue.
|
|
164
|
+
*
|
|
165
|
+
* Uses `execFileSync` (not `exec`) to prevent shell injection: the command
|
|
166
|
+
* and arguments are passed as separate parameters so no shell is spawned.
|
|
167
|
+
*/
|
|
168
|
+
function installSkills() {
|
|
169
|
+
try {
|
|
170
|
+
execFileSync('cleo', ['skills', 'install'], { stdio: 'inherit' });
|
|
171
|
+
process.stdout.write('CleoOS: skills install complete\n');
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// cleo may not be installed or skills may already be up to date.
|
|
175
|
+
process.stdout.write('CleoOS: skipping skills install (cleo not found or already installed)\n');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// Main
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
/**
|
|
182
|
+
* Entry point for the CleoOS postinstall script.
|
|
183
|
+
*
|
|
184
|
+
* Orchestrates directory scaffolding, extension deployment, CANT stub
|
|
185
|
+
* creation, and optional skill installation. All operations are idempotent.
|
|
186
|
+
*/
|
|
187
|
+
function main() {
|
|
188
|
+
if (!isGlobalInstall()) {
|
|
189
|
+
process.stdout.write('CleoOS: skipping postinstall (not global install)\n');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const paths = resolveCleoOsPaths();
|
|
193
|
+
const pkgRoot = resolve(__dirname, '..');
|
|
194
|
+
// 1. Scaffold XDG directories
|
|
195
|
+
for (const dir of [paths.data, paths.config, paths.extensions, paths.cant, paths.auth]) {
|
|
196
|
+
ensureDir(dir);
|
|
197
|
+
}
|
|
198
|
+
// 2. Deploy compiled extensions
|
|
199
|
+
deployExtension('cleo-cant-bridge', pkgRoot, paths.extensions);
|
|
200
|
+
deployExtension('cleo-chatroom', pkgRoot, paths.extensions);
|
|
201
|
+
// 3. Write default CANT stub (only if file does not exist)
|
|
202
|
+
scaffoldDefaultCant(paths.cant);
|
|
203
|
+
// 4. Install CleoOS skills (best-effort)
|
|
204
|
+
installSkills();
|
|
205
|
+
process.stdout.write('CleoOS: postinstall complete\n');
|
|
206
|
+
}
|
|
207
|
+
main();
|
|
208
|
+
//# sourceMappingURL=postinstall.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postinstall.js","sourceRoot":"","sources":["../src/postinstall.ts"],"names":[],"mappings":"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"}
|
package/dist/xdg.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XDG-compliant path resolution for CleoOS.
|
|
3
|
+
*
|
|
4
|
+
* Resolves:
|
|
5
|
+
* - Data: $XDG_DATA_HOME/cleo/ or ~/.local/share/cleo/
|
|
6
|
+
* - Config: $XDG_CONFIG_HOME/cleo/ or ~/.config/cleo/
|
|
7
|
+
* - Agent dir: same as data root (Pi's agentDir equivalent)
|
|
8
|
+
* - Extensions: <data>/extensions/
|
|
9
|
+
* - CANT source: <data>/cant/ (global tier)
|
|
10
|
+
*
|
|
11
|
+
* @packageDocumentation
|
|
12
|
+
*/
|
|
13
|
+
/** Resolved CleoOS filesystem paths following XDG Base Directory Specification. */
|
|
14
|
+
export interface CleoOsPaths {
|
|
15
|
+
/** XDG data home: ~/.local/share/cleo/ */
|
|
16
|
+
data: string;
|
|
17
|
+
/** XDG config home: ~/.config/cleo/ */
|
|
18
|
+
config: string;
|
|
19
|
+
/** Pi agent directory (= data root) */
|
|
20
|
+
agentDir: string;
|
|
21
|
+
/** Extensions directory: <data>/extensions/ */
|
|
22
|
+
extensions: string;
|
|
23
|
+
/** Global CANT source: <data>/cant/ */
|
|
24
|
+
cant: string;
|
|
25
|
+
/** Auth/keystore directory: <config>/auth/ */
|
|
26
|
+
auth: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Resolve CleoOS filesystem paths using XDG Base Directory Specification.
|
|
30
|
+
*
|
|
31
|
+
* Respects `XDG_DATA_HOME` and `XDG_CONFIG_HOME` environment variables
|
|
32
|
+
* when set, falling back to the XDG defaults (`~/.local/share/` and
|
|
33
|
+
* `~/.config/` respectively).
|
|
34
|
+
*
|
|
35
|
+
* @returns Resolved paths for all CleoOS directories.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveCleoOsPaths(): CleoOsPaths;
|
|
38
|
+
//# sourceMappingURL=xdg.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xdg.d.ts","sourceRoot":"","sources":["../src/xdg.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,mFAAmF;AACnF,MAAM,WAAW,WAAW;IAC1B,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,uCAAuC;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,+CAA+C;IAC/C,UAAU,EAAE,MAAM,CAAC;IACnB,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,IAAI,WAAW,CAgBhD"}
|
package/dist/xdg.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XDG-compliant path resolution for CleoOS.
|
|
3
|
+
*
|
|
4
|
+
* Resolves:
|
|
5
|
+
* - Data: $XDG_DATA_HOME/cleo/ or ~/.local/share/cleo/
|
|
6
|
+
* - Config: $XDG_CONFIG_HOME/cleo/ or ~/.config/cleo/
|
|
7
|
+
* - Agent dir: same as data root (Pi's agentDir equivalent)
|
|
8
|
+
* - Extensions: <data>/extensions/
|
|
9
|
+
* - CANT source: <data>/cant/ (global tier)
|
|
10
|
+
*
|
|
11
|
+
* @packageDocumentation
|
|
12
|
+
*/
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
/**
|
|
16
|
+
* Resolve CleoOS filesystem paths using XDG Base Directory Specification.
|
|
17
|
+
*
|
|
18
|
+
* Respects `XDG_DATA_HOME` and `XDG_CONFIG_HOME` environment variables
|
|
19
|
+
* when set, falling back to the XDG defaults (`~/.local/share/` and
|
|
20
|
+
* `~/.config/` respectively).
|
|
21
|
+
*
|
|
22
|
+
* @returns Resolved paths for all CleoOS directories.
|
|
23
|
+
*/
|
|
24
|
+
export function resolveCleoOsPaths() {
|
|
25
|
+
const home = homedir();
|
|
26
|
+
const xdgData = process.env['XDG_DATA_HOME'] ?? join(home, '.local', 'share');
|
|
27
|
+
const xdgConfig = process.env['XDG_CONFIG_HOME'] ?? join(home, '.config');
|
|
28
|
+
const data = join(xdgData, 'cleo');
|
|
29
|
+
const config = join(xdgConfig, 'cleo');
|
|
30
|
+
return {
|
|
31
|
+
data,
|
|
32
|
+
config,
|
|
33
|
+
agentDir: data,
|
|
34
|
+
extensions: join(data, 'extensions'),
|
|
35
|
+
cant: join(data, 'cant'),
|
|
36
|
+
auth: join(config, 'auth'),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=xdg.js.map
|
package/dist/xdg.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xdg.js","sourceRoot":"","sources":["../src/xdg.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAkBjC;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB;IAChC,MAAM,IAAI,GAAG,OAAO,EAAE,CAAC;IACvB,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;IAC9E,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAE1E,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IACnC,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IAEvC,OAAO;QACL,IAAI;QACJ,MAAM;QACN,QAAQ,EAAE,IAAI;QACd,UAAU,EAAE,IAAI,CAAC,IAAI,EAAE,YAAY,CAAC;QACpC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC;QACxB,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC;KAC3B,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CleoOS CANT bridge — Wave 2 Pi extension.
|
|
3
|
+
*
|
|
4
|
+
* CANONICAL LOCATION: `packages/cleo-os/extensions/cleo-cant-bridge.ts`
|
|
5
|
+
*
|
|
6
|
+
* This file was copied from
|
|
7
|
+
* `packages/cleo/templates/cleoos-hub/pi-extensions/cleo-cant-bridge.ts`
|
|
8
|
+
* (T393). The template path is kept for reference but this file is the
|
|
9
|
+
* authoritative source. A future cleanup wave (post-T381) should remove
|
|
10
|
+
* the template copy once all consumers have migrated.
|
|
11
|
+
*
|
|
12
|
+
* Installed to: $XDG_DATA_HOME/cleo/extensions/cleo-cant-bridge.js
|
|
13
|
+
* Loaded by: Pi via `--extension <path>` injected by CleoOS cli.ts
|
|
14
|
+
*
|
|
15
|
+
* This bridge discovers `.cant` files in the project's `.cleo/cant/`
|
|
16
|
+
* directory at session start, compiles them via `@cleocode/cant`'s
|
|
17
|
+
* `compileBundle()`, and appends the compiled declarations to Pi's
|
|
18
|
+
* system prompt on `before_agent_start`. This gives the LLM awareness
|
|
19
|
+
* of all declared agents, teams, and tools without hand-authored
|
|
20
|
+
* protocol text.
|
|
21
|
+
*
|
|
22
|
+
* Wave 2 scope:
|
|
23
|
+
* - Scans project tier only: `<cwd>/.cleo/cant/` (recursive)
|
|
24
|
+
* - Three-tier resolution (global, user, project) is Wave 5
|
|
25
|
+
* - Prompt strategy: APPEND (per ULTRAPLAN L6, never replace)
|
|
26
|
+
*
|
|
27
|
+
* Wave 8 additions (T420):
|
|
28
|
+
* - validate-on-load mental-model injection
|
|
29
|
+
* - When the spawned agent's CANT definition has a `mentalModel` block,
|
|
30
|
+
* fetches prior mental-model observations via memoryFind and injects
|
|
31
|
+
* them into the Pi system prompt with VALIDATE_ON_LOAD_PREAMBLE.
|
|
32
|
+
* - Exports `VALIDATE_ON_LOAD_PREAMBLE` and `buildMentalModelInjection`
|
|
33
|
+
* for testability (T421).
|
|
34
|
+
*
|
|
35
|
+
* Requirements:
|
|
36
|
+
* - `@cleocode/cant` must be installed (provides `compileBundle`)
|
|
37
|
+
* - Pi coding agent runtime (`@mariozechner/pi-coding-agent`)
|
|
38
|
+
*
|
|
39
|
+
* Guardrails:
|
|
40
|
+
* - Best-effort: if `@cleocode/cant` is not installed or `.cleo/cant`
|
|
41
|
+
* does not exist, the bridge is a no-op. NEVER crash Pi.
|
|
42
|
+
* - NO top-level await; all work happens inside event handlers.
|
|
43
|
+
* - APPEND to system prompt, never replace.
|
|
44
|
+
*
|
|
45
|
+
* @packageDocumentation
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
49
|
+
import { join } from "node:path";
|
|
50
|
+
import type {
|
|
51
|
+
ExtensionAPI,
|
|
52
|
+
ExtensionContext,
|
|
53
|
+
} from "@mariozechner/pi-coding-agent";
|
|
54
|
+
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// T420: validate-on-load constants and pure helpers
|
|
57
|
+
// ============================================================================
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Preamble text injected into the Pi system prompt when an agent has a
|
|
61
|
+
* `mental_model:` CANT block. The agent MUST re-evaluate each observation
|
|
62
|
+
* against the current project state before acting.
|
|
63
|
+
*
|
|
64
|
+
* Exported so empirical tests (T421) can assert on its presence.
|
|
65
|
+
*/
|
|
66
|
+
export const VALIDATE_ON_LOAD_PREAMBLE =
|
|
67
|
+
"===== MENTAL MODEL (validate-on-load) =====\n" +
|
|
68
|
+
"These are your prior observations, patterns, and learnings for this project.\n" +
|
|
69
|
+
"Before acting, you MUST re-evaluate each entry against current project state.\n" +
|
|
70
|
+
"If an entry is stale, note it and proceed with fresh understanding.";
|
|
71
|
+
|
|
72
|
+
/** Minimal observation shape returned by memoryFind / searchBrainCompact. */
|
|
73
|
+
export interface MentalModelObservation {
|
|
74
|
+
id: string;
|
|
75
|
+
type: string;
|
|
76
|
+
title: string;
|
|
77
|
+
date?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Build the validate-on-load mental-model injection string.
|
|
82
|
+
*
|
|
83
|
+
* Pure function — no I/O, safe to call in tests without a real DB.
|
|
84
|
+
*
|
|
85
|
+
* @param agentName - Name of the spawned agent (used in the header line).
|
|
86
|
+
* @param observations - Prior mental-model observations to list.
|
|
87
|
+
* @returns System-prompt block containing the preamble and numbered observations,
|
|
88
|
+
* or an empty string when `observations` is empty.
|
|
89
|
+
*/
|
|
90
|
+
export function buildMentalModelInjection(
|
|
91
|
+
agentName: string,
|
|
92
|
+
observations: MentalModelObservation[],
|
|
93
|
+
): string {
|
|
94
|
+
if (observations.length === 0) return "";
|
|
95
|
+
|
|
96
|
+
const lines: string[] = [
|
|
97
|
+
"",
|
|
98
|
+
`// Agent: ${agentName}`,
|
|
99
|
+
VALIDATE_ON_LOAD_PREAMBLE,
|
|
100
|
+
"",
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
for (let i = 0; i < observations.length; i++) {
|
|
104
|
+
const obs = observations[i];
|
|
105
|
+
const datePart = obs.date ? ` [${obs.date}]` : "";
|
|
106
|
+
lines.push(`${i + 1}. [${obs.id}] (${obs.type})${datePart}: ${obs.title}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
lines.push("===== END MENTAL MODEL =====");
|
|
110
|
+
|
|
111
|
+
return lines.join("\n");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ============================================================================
|
|
115
|
+
// T424: Path-ACL helpers (pure, no external deps)
|
|
116
|
+
// ============================================================================
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Path-scoped file permissions shape expected in an agentDef at runtime.
|
|
120
|
+
*
|
|
121
|
+
* Mirrors `PathPermissions` from `@cleocode/cant` (T423).
|
|
122
|
+
* Kept inline here to avoid a direct runtime import in the Pi extension context.
|
|
123
|
+
*
|
|
124
|
+
* @task T424
|
|
125
|
+
*/
|
|
126
|
+
interface AgentFilePermissions {
|
|
127
|
+
/** Glob patterns the agent may write to. Empty array = no writes allowed. */
|
|
128
|
+
write?: string[];
|
|
129
|
+
/** Glob patterns the agent may read from. */
|
|
130
|
+
read?: string[];
|
|
131
|
+
/** Glob patterns the agent may delete. */
|
|
132
|
+
delete?: string[];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Convert a glob pattern to a RegExp for path matching.
|
|
137
|
+
*
|
|
138
|
+
* Supports the subset of glob syntax used in CANT file permissions:
|
|
139
|
+
* - `**` matches any path segment sequence (including none)
|
|
140
|
+
* - `*` matches any characters within a single path segment
|
|
141
|
+
* - `?` matches a single character
|
|
142
|
+
* - All other characters are treated as literals
|
|
143
|
+
*
|
|
144
|
+
* @param glob - The glob pattern string.
|
|
145
|
+
* @returns A RegExp that tests absolute or relative file paths.
|
|
146
|
+
*/
|
|
147
|
+
function globToRegExp(glob: string): RegExp {
|
|
148
|
+
// Escape special regex characters except our glob specials
|
|
149
|
+
let regexStr = "";
|
|
150
|
+
let i = 0;
|
|
151
|
+
while (i < glob.length) {
|
|
152
|
+
const char = glob[i];
|
|
153
|
+
if (char === "*" && glob[i + 1] === "*") {
|
|
154
|
+
// ** matches everything including path separators
|
|
155
|
+
regexStr += ".*";
|
|
156
|
+
i += 2;
|
|
157
|
+
// Skip optional trailing slash after **
|
|
158
|
+
if (glob[i] === "/") i++;
|
|
159
|
+
} else if (char === "*") {
|
|
160
|
+
// * matches anything except path separator
|
|
161
|
+
regexStr += "[^/]*";
|
|
162
|
+
i++;
|
|
163
|
+
} else if (char === "?") {
|
|
164
|
+
regexStr += "[^/]";
|
|
165
|
+
i++;
|
|
166
|
+
} else if (/[.+^${}()|[\]\\]/.test(char)) {
|
|
167
|
+
regexStr += "\\" + char;
|
|
168
|
+
i++;
|
|
169
|
+
} else {
|
|
170
|
+
regexStr += char;
|
|
171
|
+
i++;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return new RegExp("^" + regexStr + "$");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Test whether a file path matches any of the provided glob patterns.
|
|
179
|
+
*
|
|
180
|
+
* Normalises the path to use forward slashes. Returns `false` immediately
|
|
181
|
+
* when `globs` is an empty array (default-deny for empty write lists).
|
|
182
|
+
*
|
|
183
|
+
* @param filePath - The file path to test (absolute or relative).
|
|
184
|
+
* @param globs - The glob patterns to test against.
|
|
185
|
+
* @returns `true` if `filePath` matches at least one glob pattern.
|
|
186
|
+
*/
|
|
187
|
+
function matchesAnyGlob(filePath: string, globs: string[]): boolean {
|
|
188
|
+
if (globs.length === 0) return false;
|
|
189
|
+
// Normalise separators; strip leading slash for relative matching
|
|
190
|
+
const normalized = filePath.replace(/\\/g, "/").replace(/^\//, "");
|
|
191
|
+
for (const glob of globs) {
|
|
192
|
+
if (globToRegExp(glob).test(normalized)) return true;
|
|
193
|
+
}
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Attempt to extract the target file path from a Pi tool_call event.
|
|
199
|
+
*
|
|
200
|
+
* Handles the three writable tool shapes:
|
|
201
|
+
* - `Edit`: `{ input: { file_path: string } }` or `{ filePath: string }`
|
|
202
|
+
* - `Write`: `{ input: { file_path: string } }` or `{ filePath: string }`
|
|
203
|
+
* - `Bash`: best-effort scan of the command string for write destinations
|
|
204
|
+
*
|
|
205
|
+
* Returns `null` when the path cannot be determined (allow-by-default for Bash
|
|
206
|
+
* when the destination is ambiguous).
|
|
207
|
+
*
|
|
208
|
+
* @param toolName - The tool being invoked ("Edit", "Write", or "Bash").
|
|
209
|
+
* @param toolInput - The raw tool input object.
|
|
210
|
+
* @returns The extracted file path, or `null` if not determinable.
|
|
211
|
+
*/
|
|
212
|
+
function extractTargetPath(
|
|
213
|
+
toolName: string,
|
|
214
|
+
toolInput: Record<string, unknown> | undefined,
|
|
215
|
+
): string | null {
|
|
216
|
+
if (!toolInput) return null;
|
|
217
|
+
|
|
218
|
+
if (toolName === "Edit" || toolName === "Write") {
|
|
219
|
+
// Pi uses snake_case in the actual tool call input
|
|
220
|
+
if (typeof toolInput["file_path"] === "string") return toolInput["file_path"];
|
|
221
|
+
// camelCase fallback (bridge convention)
|
|
222
|
+
if (typeof toolInput["filePath"] === "string") return toolInput["filePath"];
|
|
223
|
+
// path fallback
|
|
224
|
+
if (typeof toolInput["path"] === "string") return toolInput["path"];
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (toolName === "Bash") {
|
|
229
|
+
const cmd = typeof toolInput["command"] === "string" ? toolInput["command"] : null;
|
|
230
|
+
if (!cmd) return null;
|
|
231
|
+
|
|
232
|
+
// Detect common write patterns: redirection, tee, cp/mv destination
|
|
233
|
+
// Best-effort: return the first detected destination path.
|
|
234
|
+
// If ambiguous, return null (allow-by-default for Bash).
|
|
235
|
+
const redirectMatch = cmd.match(/>\s*["']?([^\s"';&|]+)/);
|
|
236
|
+
if (redirectMatch?.[1]) return redirectMatch[1];
|
|
237
|
+
|
|
238
|
+
const teeMatch = cmd.match(/\btee\s+(?:-a\s+)?["']?([^\s"';&|]+)/);
|
|
239
|
+
if (teeMatch?.[1]) return teeMatch[1];
|
|
240
|
+
|
|
241
|
+
// cp/mv destination is the last argument — very heuristic
|
|
242
|
+
const cpMvMatch = cmd.match(/\b(?:cp|mv)\s+\S+\s+["']?([^\s"';&|]+)/);
|
|
243
|
+
if (cpMvMatch?.[1]) return cpMvMatch[1];
|
|
244
|
+
|
|
245
|
+
return null; // Cannot determine — allow (workers self-report)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ============================================================================
|
|
252
|
+
// Internal state
|
|
253
|
+
// ============================================================================
|
|
254
|
+
|
|
255
|
+
/** Cached system prompt addendum from the last session_start compilation. */
|
|
256
|
+
let bundlePrompt: string | null = null;
|
|
257
|
+
|
|
258
|
+
/** Diagnostic summary cached for /cant:bundle-info. */
|
|
259
|
+
let lastDiagnosticSummary: string | null = null;
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Recursively discover `.cant` files in a directory.
|
|
263
|
+
*
|
|
264
|
+
* @param dir - The directory to scan recursively.
|
|
265
|
+
* @returns An array of absolute paths to `.cant` files found.
|
|
266
|
+
*/
|
|
267
|
+
function discoverCantFiles(dir: string): string[] {
|
|
268
|
+
try {
|
|
269
|
+
const entries = readdirSync(dir, { recursive: true, withFileTypes: true });
|
|
270
|
+
const files: string[] = [];
|
|
271
|
+
for (const entry of entries) {
|
|
272
|
+
if (entry.isFile() && entry.name.endsWith(".cant")) {
|
|
273
|
+
// Node 24+ recursive readdir returns entries with parentPath
|
|
274
|
+
const parent = (entry as unknown as { parentPath?: string }).parentPath ?? dir;
|
|
275
|
+
files.push(join(parent, entry.name));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return files;
|
|
279
|
+
} catch {
|
|
280
|
+
return [];
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ============================================================================
|
|
285
|
+
// T420: mental-model injection helper (async, calls memoryFind)
|
|
286
|
+
// ============================================================================
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Fetch prior mental-model observations for an agent and build the
|
|
290
|
+
* validate-on-load injection block.
|
|
291
|
+
*
|
|
292
|
+
* Called in `before_agent_start` when the agent has a `mentalModel` CANT block.
|
|
293
|
+
* Best-effort: returns empty string on any failure so Pi is never blocked.
|
|
294
|
+
*
|
|
295
|
+
* @param agentName - Name of the spawned agent.
|
|
296
|
+
* @param projectRoot - Project root directory for brain.db access.
|
|
297
|
+
* @returns The validate-on-load system-prompt block, or "" on failure/empty.
|
|
298
|
+
*/
|
|
299
|
+
async function fetchMentalModelInjection(
|
|
300
|
+
agentName: string,
|
|
301
|
+
projectRoot: string,
|
|
302
|
+
): Promise<string> {
|
|
303
|
+
try {
|
|
304
|
+
// Lazy import: @cleocode/core may not be present in all environments.
|
|
305
|
+
// memoryFind is the engine-compat wrapper (T418) that accepts `agent`.
|
|
306
|
+
const coreModule = (await import("@cleocode/core")) as {
|
|
307
|
+
memoryFind?: (
|
|
308
|
+
params: {
|
|
309
|
+
query: string;
|
|
310
|
+
agent?: string;
|
|
311
|
+
limit?: number;
|
|
312
|
+
tables?: string[];
|
|
313
|
+
},
|
|
314
|
+
projectRoot?: string,
|
|
315
|
+
) => Promise<{
|
|
316
|
+
success: boolean;
|
|
317
|
+
data?: {
|
|
318
|
+
results?: MentalModelObservation[];
|
|
319
|
+
};
|
|
320
|
+
}>;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
if (typeof coreModule.memoryFind !== "function") return "";
|
|
324
|
+
|
|
325
|
+
// Fetch the 10 most recent mental-model observations for this agent.
|
|
326
|
+
// Use tables filter to avoid decisions/patterns/learnings which are
|
|
327
|
+
// not agent-scoped in the current schema.
|
|
328
|
+
const result = await coreModule.memoryFind(
|
|
329
|
+
{
|
|
330
|
+
query: agentName,
|
|
331
|
+
agent: agentName,
|
|
332
|
+
limit: 10,
|
|
333
|
+
tables: ["observations"],
|
|
334
|
+
},
|
|
335
|
+
projectRoot,
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
if (!result.success || !result.data?.results?.length) return "";
|
|
339
|
+
|
|
340
|
+
return buildMentalModelInjection(agentName, result.data.results);
|
|
341
|
+
} catch {
|
|
342
|
+
// Best-effort — never crash Pi
|
|
343
|
+
return "";
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ============================================================================
|
|
348
|
+
// Pi extension factory
|
|
349
|
+
// ============================================================================
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Pi extension factory for the CleoOS CANT bridge.
|
|
353
|
+
*
|
|
354
|
+
* Registers event handlers for `session_start` (compile `.cant` files)
|
|
355
|
+
* and `before_agent_start` (append compiled bundle + mental-model injection
|
|
356
|
+
* to system prompt). Also registers a `/cant:bundle-info` command for
|
|
357
|
+
* introspection.
|
|
358
|
+
*
|
|
359
|
+
* @param pi - The Pi extension API instance.
|
|
360
|
+
*/
|
|
361
|
+
export default function (pi: ExtensionAPI): void {
|
|
362
|
+
// session_start: discover and compile .cant files from the project tier
|
|
363
|
+
pi.on("session_start", async (_event: unknown, ctx: ExtensionContext) => {
|
|
364
|
+
bundlePrompt = null;
|
|
365
|
+
lastDiagnosticSummary = null;
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
const cantDir = join(ctx.cwd, ".cleo", "cant");
|
|
369
|
+
if (!existsSync(cantDir)) return;
|
|
370
|
+
|
|
371
|
+
const files = discoverCantFiles(cantDir);
|
|
372
|
+
if (files.length === 0) return;
|
|
373
|
+
|
|
374
|
+
// Dynamic import: @cleocode/cant may not be installed in all environments
|
|
375
|
+
const cantModule = (await import("@cleocode/cant")) as {
|
|
376
|
+
compileBundle: (paths: string[]) => Promise<{
|
|
377
|
+
renderSystemPrompt: () => string;
|
|
378
|
+
diagnostics: Array<{ severity: string; message: string; sourcePath: string }>;
|
|
379
|
+
agents: Array<{ name: string }>;
|
|
380
|
+
teams: Array<{ name: string }>;
|
|
381
|
+
tools: Array<{ name: string }>;
|
|
382
|
+
valid: boolean;
|
|
383
|
+
}>;
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const bundle = await cantModule.compileBundle(files);
|
|
387
|
+
const prompt = bundle.renderSystemPrompt();
|
|
388
|
+
|
|
389
|
+
if (prompt.length > 0) {
|
|
390
|
+
bundlePrompt = prompt;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Build diagnostic summary
|
|
394
|
+
const errorDiags = bundle.diagnostics.filter((d) => d.severity === "error");
|
|
395
|
+
const warnDiags = bundle.diagnostics.filter((d) => d.severity === "warning");
|
|
396
|
+
lastDiagnosticSummary = [
|
|
397
|
+
`Files: ${files.length}`,
|
|
398
|
+
`Agents: ${bundle.agents.length}`,
|
|
399
|
+
`Teams: ${bundle.teams.length}`,
|
|
400
|
+
`Tools: ${bundle.tools.length}`,
|
|
401
|
+
`Valid: ${bundle.valid}`,
|
|
402
|
+
`Errors: ${errorDiags.length}`,
|
|
403
|
+
`Warnings: ${warnDiags.length}`,
|
|
404
|
+
].join(", ");
|
|
405
|
+
|
|
406
|
+
// Notify on errors
|
|
407
|
+
if (errorDiags.length > 0 && ctx.hasUI) {
|
|
408
|
+
ctx.ui.notify(
|
|
409
|
+
`CleoOS CANT bridge: ${errorDiags.length} validation error(s) in .cleo/cant/`,
|
|
410
|
+
"warning",
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Success notification
|
|
415
|
+
if (ctx.hasUI) {
|
|
416
|
+
ctx.ui.setStatus(
|
|
417
|
+
"cleo-cant-bridge",
|
|
418
|
+
`CANT: ${bundle.agents.length} agent(s), ${files.length} file(s)`,
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
} catch (err: unknown) {
|
|
422
|
+
// Best-effort: never crash Pi
|
|
423
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
424
|
+
if (ctx.hasUI) {
|
|
425
|
+
ctx.ui.notify(`CleoOS CANT bridge: ${message}`, "warning");
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// before_agent_start: APPEND compiled bundle prompt + mental-model injection
|
|
431
|
+
// to system prompt (per ULTRAPLAN L6, never replace)
|
|
432
|
+
pi.on(
|
|
433
|
+
"before_agent_start",
|
|
434
|
+
async (
|
|
435
|
+
event: {
|
|
436
|
+
systemPrompt?: string;
|
|
437
|
+
agentName?: string;
|
|
438
|
+
/** T420: agent CANT definition, if resolved by Pi runtime. */
|
|
439
|
+
agentDef?: {
|
|
440
|
+
/** mentalModel block presence signals validate-on-load injection. */
|
|
441
|
+
mentalModel?: unknown;
|
|
442
|
+
};
|
|
443
|
+
/** Project root injected by Pi when available. */
|
|
444
|
+
projectRoot?: string;
|
|
445
|
+
},
|
|
446
|
+
ctx?: ExtensionContext,
|
|
447
|
+
) => {
|
|
448
|
+
const existingPrompt = event.systemPrompt ?? "";
|
|
449
|
+
let appendix = "";
|
|
450
|
+
|
|
451
|
+
// APPEND CANT bundle prompt
|
|
452
|
+
if (bundlePrompt) {
|
|
453
|
+
appendix += "\n\n" + bundlePrompt;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// T420: validate-on-load mental-model injection.
|
|
457
|
+
// Inject when the agent has a `mentalModel` CANT block.
|
|
458
|
+
const agentName = event.agentName;
|
|
459
|
+
const hasMentalModel =
|
|
460
|
+
agentName !== undefined &&
|
|
461
|
+
agentName !== "" &&
|
|
462
|
+
event.agentDef?.mentalModel !== undefined;
|
|
463
|
+
|
|
464
|
+
if (hasMentalModel && agentName) {
|
|
465
|
+
// Resolve project root: prefer explicit field, fall back to ctx.cwd
|
|
466
|
+
const projectRoot = event.projectRoot ?? ctx?.cwd ?? "";
|
|
467
|
+
if (projectRoot) {
|
|
468
|
+
const mentalModelBlock = await fetchMentalModelInjection(agentName, projectRoot);
|
|
469
|
+
if (mentalModelBlock) {
|
|
470
|
+
appendix += mentalModelBlock;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (!appendix) return {};
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
systemPrompt: existingPrompt + appendix,
|
|
479
|
+
};
|
|
480
|
+
},
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
// tool_call: ULTRAPLAN §10.3 — Lead agents MUST NOT execute Edit/Write/Bash.
|
|
484
|
+
// T424: Worker agents with declared file permissions are restricted to their
|
|
485
|
+
// declared write globs. Leads dispatch; workers execute within scope.
|
|
486
|
+
// Fires on every Pi tool_call event.
|
|
487
|
+
// The before_agent_start handler (T420 validate-on-load) is NOT touched here.
|
|
488
|
+
pi.on(
|
|
489
|
+
"tool_call",
|
|
490
|
+
async (event: {
|
|
491
|
+
/** CANT agent definition resolved by Pi at spawn time, if available. */
|
|
492
|
+
agentDef?: {
|
|
493
|
+
/** Tier role declared in the .cant file (e.g. "lead", "worker", "orchestrator"). */
|
|
494
|
+
role?: string;
|
|
495
|
+
/** Path-scoped file permissions declared in the .cant file (T423). */
|
|
496
|
+
filePermissions?: AgentFilePermissions;
|
|
497
|
+
/** Agent name for diagnostic messages. */
|
|
498
|
+
name?: string;
|
|
499
|
+
};
|
|
500
|
+
/** The tool name being invoked (e.g. "Edit", "Write", "Bash"). */
|
|
501
|
+
toolName?: string;
|
|
502
|
+
/** The raw tool input object (contains file_path for Edit/Write, command for Bash). */
|
|
503
|
+
toolInput?: Record<string, unknown>;
|
|
504
|
+
}) => {
|
|
505
|
+
const agentDef = event.agentDef;
|
|
506
|
+
// No agentDef = no restrictions (hook is a no-op).
|
|
507
|
+
if (!agentDef) return {};
|
|
508
|
+
|
|
509
|
+
const toolName = event.toolName ?? "";
|
|
510
|
+
const BLOCKED_TOOLS = ["Edit", "Write", "Bash"];
|
|
511
|
+
|
|
512
|
+
// ── W7b: Lead blocking ─────────────────────────────────────────────
|
|
513
|
+
// Only restrict agents whose CANT role is explicitly "lead".
|
|
514
|
+
// Non-lead roles (worker, orchestrator, undefined) pass this gate.
|
|
515
|
+
if (agentDef.role !== "lead") {
|
|
516
|
+
// Fall through to the T424 worker path ACL check below.
|
|
517
|
+
} else {
|
|
518
|
+
// Lead role: block Edit/Write/Bash entirely.
|
|
519
|
+
if (!BLOCKED_TOOLS.includes(toolName)) return {};
|
|
520
|
+
|
|
521
|
+
// Reject the tool call with a LAFS error envelope.
|
|
522
|
+
return {
|
|
523
|
+
rejected: true,
|
|
524
|
+
error: {
|
|
525
|
+
code: 70,
|
|
526
|
+
codeName: "E_LEAD_TOOL_BLOCKED",
|
|
527
|
+
message: `Lead agents cannot execute ${toolName} — dispatch to a worker instead`,
|
|
528
|
+
fix: "Use the delegate tool to spawn a worker agent for this work",
|
|
529
|
+
},
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// ── T424: Worker path ACL ──────────────────────────────────────────
|
|
534
|
+
// Workers with declared file permissions can only write inside their
|
|
535
|
+
// declared globs. Applies to Edit, Write, and Bash (best-effort).
|
|
536
|
+
if (
|
|
537
|
+
agentDef.role === "worker" &&
|
|
538
|
+
agentDef.filePermissions !== undefined &&
|
|
539
|
+
BLOCKED_TOOLS.includes(toolName)
|
|
540
|
+
) {
|
|
541
|
+
const writeGlobs = agentDef.filePermissions.write;
|
|
542
|
+
// `undefined` write field = no declared write ACL = allow through.
|
|
543
|
+
// Empty array [] = explicit no-writes = default-deny.
|
|
544
|
+
if (writeGlobs !== undefined) {
|
|
545
|
+
const targetPath = extractTargetPath(toolName, event.toolInput);
|
|
546
|
+
if (targetPath !== null && !matchesAnyGlob(targetPath, writeGlobs)) {
|
|
547
|
+
const agentName = agentDef.name ?? "worker";
|
|
548
|
+
const scopeList =
|
|
549
|
+
writeGlobs.length > 0 ? writeGlobs.join(", ") : "(none — this worker is read-only)";
|
|
550
|
+
return {
|
|
551
|
+
rejected: true,
|
|
552
|
+
error: {
|
|
553
|
+
code: 71,
|
|
554
|
+
codeName: "E_WORKER_PATH_ACL_VIOLATION",
|
|
555
|
+
message: `Worker ${agentName} is not allowed to write to ${targetPath}`,
|
|
556
|
+
fix:
|
|
557
|
+
`This worker can only write inside: ${scopeList}. ` +
|
|
558
|
+
"Either update the worker's permissions.files.write glob in " +
|
|
559
|
+
".cleo/teams.cant, or dispatch to a different worker with matching scope.",
|
|
560
|
+
},
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return {};
|
|
567
|
+
},
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
// /cant:bundle-info — introspection command
|
|
571
|
+
pi.registerCommand("cant:bundle-info", {
|
|
572
|
+
description: "Show the state of the CANT bundle compiled at session start",
|
|
573
|
+
handler: async (
|
|
574
|
+
_args: string,
|
|
575
|
+
ctx: ExtensionContext & { hasUI: boolean; signal?: AbortSignal },
|
|
576
|
+
) => {
|
|
577
|
+
const content = lastDiagnosticSummary
|
|
578
|
+
? `CANT Bundle: ${lastDiagnosticSummary}`
|
|
579
|
+
: "CANT Bundle: no .cant files compiled (check .cleo/cant/ directory)";
|
|
580
|
+
pi.sendMessage(
|
|
581
|
+
{ customType: "cleo-cant-bundle-info", content, display: true },
|
|
582
|
+
{ triggerTurn: false },
|
|
583
|
+
);
|
|
584
|
+
if (ctx.hasUI) {
|
|
585
|
+
ctx.ui.notify(
|
|
586
|
+
lastDiagnosticSummary ? "CANT bundle loaded" : "No CANT bundle",
|
|
587
|
+
"info",
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
},
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
// session_shutdown: clear cached state
|
|
594
|
+
pi.on("session_shutdown", async () => {
|
|
595
|
+
bundlePrompt = null;
|
|
596
|
+
lastDiagnosticSummary = null;
|
|
597
|
+
});
|
|
598
|
+
}
|
|
@@ -36,8 +36,14 @@ import { Type } from "@sinclair/typebox";
|
|
|
36
36
|
// Message model
|
|
37
37
|
// ---------------------------------------------------------------------------
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Tier role of an agent in the 3-tier hierarchy (ULTRAPLAN §10).
|
|
41
|
+
* Used to apply distinct TUI styling per tier.
|
|
42
|
+
*/
|
|
43
|
+
export type AgentTierRole = "orchestrator" | "lead" | "worker";
|
|
44
|
+
|
|
39
45
|
/** A single inter-agent chat message. */
|
|
40
|
-
interface ChatMessage {
|
|
46
|
+
export interface ChatMessage {
|
|
41
47
|
/** ISO-8601 timestamp of when the message was created. */
|
|
42
48
|
timestamp: string;
|
|
43
49
|
/** Name of the sending agent. */
|
|
@@ -48,6 +54,14 @@ interface ChatMessage {
|
|
|
48
54
|
channel: "send_to_lead" | "broadcast_to_team" | "report_to_orchestrator" | "query_peer";
|
|
49
55
|
/** The message text. */
|
|
50
56
|
text: string;
|
|
57
|
+
/**
|
|
58
|
+
* Optional tier role of the sending agent.
|
|
59
|
+
*
|
|
60
|
+
* When present, the TUI row is prefixed and (if ANSI is available)
|
|
61
|
+
* coloured by tier: orchestrator = green ([O]), lead = yellow ([L]),
|
|
62
|
+
* worker = blue ([W]). Defaults to "worker" when absent.
|
|
63
|
+
*/
|
|
64
|
+
role?: AgentTierRole;
|
|
51
65
|
}
|
|
52
66
|
|
|
53
67
|
// ---------------------------------------------------------------------------
|
|
@@ -85,15 +99,40 @@ function recordMessage(msg: ChatMessage): void {
|
|
|
85
99
|
}
|
|
86
100
|
}
|
|
87
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Return the single-character tier prefix for a chat message row.
|
|
104
|
+
*
|
|
105
|
+
* - `[O]` orchestrator (green in ANSI-capable terminals)
|
|
106
|
+
* - `[L]` lead (yellow)
|
|
107
|
+
* - `[W]` worker (blue, default)
|
|
108
|
+
*
|
|
109
|
+
* @param role - The sending agent's tier role, or `undefined` to default to worker.
|
|
110
|
+
* @returns The three-character prefix string.
|
|
111
|
+
*/
|
|
112
|
+
export function tierPrefix(role: AgentTierRole | undefined): string {
|
|
113
|
+
switch (role) {
|
|
114
|
+
case "orchestrator":
|
|
115
|
+
return "[O]";
|
|
116
|
+
case "lead":
|
|
117
|
+
return "[L]";
|
|
118
|
+
default:
|
|
119
|
+
return "[W]";
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
88
123
|
/**
|
|
89
124
|
* Format a chat message for TUI display.
|
|
90
125
|
*
|
|
126
|
+
* Each row is prefixed with a tier indicator ([O]/[L]/[W]) so orchestrator,
|
|
127
|
+
* lead, and worker traffic is visually distinct in the chat panel (ULTRAPLAN §13).
|
|
128
|
+
*
|
|
91
129
|
* @param msg - The message to format.
|
|
92
130
|
* @returns A single-line string representation.
|
|
93
131
|
*/
|
|
94
|
-
function formatMessage(msg: ChatMessage): string {
|
|
132
|
+
export function formatMessage(msg: ChatMessage): string {
|
|
95
133
|
const time = msg.timestamp.slice(11, 19);
|
|
96
|
-
|
|
134
|
+
const prefix = tierPrefix(msg.role);
|
|
135
|
+
return `${prefix} [${time}] ${msg.from} -> ${msg.to}: ${msg.text}`;
|
|
97
136
|
}
|
|
98
137
|
|
|
99
138
|
/**
|
|
@@ -123,12 +162,26 @@ const SendToLeadParams = Type.Object({
|
|
|
123
162
|
message: Type.String({ description: "Message to send to your team lead" }),
|
|
124
163
|
from: Type.String({ description: "Your agent name" }),
|
|
125
164
|
lead: Type.String({ description: "Name of the lead agent" }),
|
|
165
|
+
role: Type.Optional(
|
|
166
|
+
Type.Union([
|
|
167
|
+
Type.Literal("orchestrator"),
|
|
168
|
+
Type.Literal("lead"),
|
|
169
|
+
Type.Literal("worker"),
|
|
170
|
+
], { description: "Tier role of the sending agent for TUI row styling" }),
|
|
171
|
+
),
|
|
126
172
|
});
|
|
127
173
|
|
|
128
174
|
const BroadcastToTeamParams = Type.Object({
|
|
129
175
|
message: Type.String({ description: "Message to broadcast to the team" }),
|
|
130
176
|
from: Type.String({ description: "Your agent name (lead)" }),
|
|
131
177
|
group: Type.String({ description: "Team group name (e.g. 'backend')" }),
|
|
178
|
+
role: Type.Optional(
|
|
179
|
+
Type.Union([
|
|
180
|
+
Type.Literal("orchestrator"),
|
|
181
|
+
Type.Literal("lead"),
|
|
182
|
+
Type.Literal("worker"),
|
|
183
|
+
], { description: "Tier role of the sending agent for TUI row styling" }),
|
|
184
|
+
),
|
|
132
185
|
});
|
|
133
186
|
|
|
134
187
|
const ReportToOrchestratorParams = Type.Object({
|
|
@@ -137,12 +190,26 @@ const ReportToOrchestratorParams = Type.Object({
|
|
|
137
190
|
orchestrator: Type.String({
|
|
138
191
|
description: "Name of the orchestrator agent",
|
|
139
192
|
}),
|
|
193
|
+
role: Type.Optional(
|
|
194
|
+
Type.Union([
|
|
195
|
+
Type.Literal("orchestrator"),
|
|
196
|
+
Type.Literal("lead"),
|
|
197
|
+
Type.Literal("worker"),
|
|
198
|
+
], { description: "Tier role of the sending agent for TUI row styling" }),
|
|
199
|
+
),
|
|
140
200
|
});
|
|
141
201
|
|
|
142
202
|
const QueryPeerParams = Type.Object({
|
|
143
203
|
message: Type.String({ description: "Query for your peer worker" }),
|
|
144
204
|
from: Type.String({ description: "Your agent name" }),
|
|
145
205
|
peer: Type.String({ description: "Name of the peer worker to query" }),
|
|
206
|
+
role: Type.Optional(
|
|
207
|
+
Type.Union([
|
|
208
|
+
Type.Literal("orchestrator"),
|
|
209
|
+
Type.Literal("lead"),
|
|
210
|
+
Type.Literal("worker"),
|
|
211
|
+
], { description: "Tier role of the sending agent for TUI row styling" }),
|
|
212
|
+
),
|
|
146
213
|
});
|
|
147
214
|
|
|
148
215
|
// ---------------------------------------------------------------------------
|
|
@@ -194,7 +261,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
194
261
|
parameters: SendToLeadParams,
|
|
195
262
|
async execute(
|
|
196
263
|
_id: string,
|
|
197
|
-
params: { message: string; from: string; lead: string },
|
|
264
|
+
params: { message: string; from: string; lead: string; role?: AgentTierRole },
|
|
198
265
|
_signal: AbortSignal,
|
|
199
266
|
_onUpdate: (text: string) => void,
|
|
200
267
|
ctx: ExtensionContext,
|
|
@@ -205,6 +272,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
205
272
|
to: params.lead,
|
|
206
273
|
channel: "send_to_lead",
|
|
207
274
|
text: params.message,
|
|
275
|
+
role: params.role,
|
|
208
276
|
};
|
|
209
277
|
recordMessage(msg);
|
|
210
278
|
renderWidget(ctx);
|
|
@@ -231,7 +299,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
231
299
|
parameters: BroadcastToTeamParams,
|
|
232
300
|
async execute(
|
|
233
301
|
_id: string,
|
|
234
|
-
params: { message: string; from: string; group: string },
|
|
302
|
+
params: { message: string; from: string; group: string; role?: AgentTierRole },
|
|
235
303
|
_signal: AbortSignal,
|
|
236
304
|
_onUpdate: (text: string) => void,
|
|
237
305
|
ctx: ExtensionContext,
|
|
@@ -242,6 +310,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
242
310
|
to: `team:${params.group}`,
|
|
243
311
|
channel: "broadcast_to_team",
|
|
244
312
|
text: params.message,
|
|
313
|
+
role: params.role,
|
|
245
314
|
};
|
|
246
315
|
recordMessage(msg);
|
|
247
316
|
renderWidget(ctx);
|
|
@@ -268,7 +337,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
268
337
|
parameters: ReportToOrchestratorParams,
|
|
269
338
|
async execute(
|
|
270
339
|
_id: string,
|
|
271
|
-
params: { message: string; from: string; orchestrator: string },
|
|
340
|
+
params: { message: string; from: string; orchestrator: string; role?: AgentTierRole },
|
|
272
341
|
_signal: AbortSignal,
|
|
273
342
|
_onUpdate: (text: string) => void,
|
|
274
343
|
ctx: ExtensionContext,
|
|
@@ -279,6 +348,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
279
348
|
to: params.orchestrator,
|
|
280
349
|
channel: "report_to_orchestrator",
|
|
281
350
|
text: params.message,
|
|
351
|
+
role: params.role,
|
|
282
352
|
};
|
|
283
353
|
recordMessage(msg);
|
|
284
354
|
renderWidget(ctx);
|
|
@@ -305,7 +375,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
305
375
|
parameters: QueryPeerParams,
|
|
306
376
|
async execute(
|
|
307
377
|
_id: string,
|
|
308
|
-
params: { message: string; from: string; peer: string },
|
|
378
|
+
params: { message: string; from: string; peer: string; role?: AgentTierRole },
|
|
309
379
|
_signal: AbortSignal,
|
|
310
380
|
_onUpdate: (text: string) => void,
|
|
311
381
|
ctx: ExtensionContext,
|
|
@@ -316,6 +386,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
316
386
|
to: params.peer,
|
|
317
387
|
channel: "query_peer",
|
|
318
388
|
text: params.message,
|
|
389
|
+
role: params.role,
|
|
319
390
|
};
|
|
320
391
|
recordMessage(msg);
|
|
321
392
|
renderWidget(ctx);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cleocode/cleo-os",
|
|
3
|
-
"version": "2026.4.
|
|
3
|
+
"version": "2026.4.16",
|
|
4
4
|
"description": "CleoOS — the batteries-included agentic development environment wrapping Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cli.js",
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
"cleoos": "dist/cli.js"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@cleocode/
|
|
12
|
-
"@cleocode/
|
|
11
|
+
"@cleocode/cant": "2026.4.16",
|
|
12
|
+
"@cleocode/cleo": "2026.4.16"
|
|
13
13
|
},
|
|
14
14
|
"peerDependencies": {
|
|
15
15
|
"@mariozechner/pi-coding-agent": ">=0.60.0"
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"typescript": "^
|
|
24
|
-
"vitest": "^4.1.
|
|
23
|
+
"typescript": "^6.0.2",
|
|
24
|
+
"vitest": "^4.1.4"
|
|
25
25
|
},
|
|
26
26
|
"engines": {
|
|
27
27
|
"node": ">=24.0.0"
|
|
@@ -41,8 +41,11 @@
|
|
|
41
41
|
"bin"
|
|
42
42
|
],
|
|
43
43
|
"scripts": {
|
|
44
|
-
"build": "tsc",
|
|
45
|
-
"
|
|
44
|
+
"build": "tsc && tsc -p tsconfig.extensions.json && tsc -p tsconfig.postinstall.json",
|
|
45
|
+
"build:src": "tsc",
|
|
46
|
+
"build:extensions": "tsc -p tsconfig.extensions.json",
|
|
47
|
+
"build:postinstall": "tsc -p tsconfig.postinstall.json",
|
|
48
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.extensions.json --noEmit && tsc -p tsconfig.postinstall.json --noEmit",
|
|
46
49
|
"test": "vitest run",
|
|
47
50
|
"postinstall": "node bin/postinstall.js"
|
|
48
51
|
}
|