@cleocode/adapters 2026.4.38 → 2026.4.40
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/cant-context.d.ts +130 -0
- package/dist/cant-context.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +491 -213
- package/dist/index.js.map +4 -4
- package/dist/providers/claude-code/adapter.d.ts.map +1 -1
- package/dist/providers/claude-code/hooks.d.ts +23 -11
- package/dist/providers/claude-code/hooks.d.ts.map +1 -1
- package/dist/providers/claude-code/spawn.d.ts.map +1 -1
- package/dist/providers/opencode/spawn.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/cant-context.test.ts +248 -0
- package/src/cant-context.ts +379 -0
- package/src/index.ts +3 -0
- package/src/providers/claude-code/adapter.ts +4 -0
- package/src/providers/claude-code/hooks.ts +135 -11
- package/src/providers/claude-code/spawn.ts +15 -1
- package/src/providers/opencode/spawn.ts +30 -11
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../../src/providers/claude-code/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,OAAO,KAAK,EACV,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,gCAAgC,EAAE,MAAM,sBAAsB,CAAC;AACxE,OAAO,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAC;AAC5D,OAAO,EAAE,2BAA2B,EAAE,MAAM,gBAAgB,CAAC;AAI7D;;;;;;;;;;;;;;GAcG;AACH,qBAAa,iBAAkB,YAAW,mBAAmB;IAC3D,kCAAkC;IAClC,QAAQ,CAAC,EAAE,iBAAiB;IAC5B,oCAAoC;IACpC,QAAQ,CAAC,IAAI,iBAAiB;IAC9B,8BAA8B;IAC9B,QAAQ,CAAC,OAAO,WAAW;IAE3B,+CAA+C;IAC/C,YAAY,EAAE,mBAAmB,CA6B/B;IAEF,8DAA8D;IAC9D,KAAK,EAAE,sBAAsB,CAAC;IAC9B,wEAAwE;IACxE,KAAK,EAAE,uBAAuB,CAAC;IAC/B,+EAA+E;IAC/E,OAAO,EAAE,yBAAyB,CAAC;IACnC,mEAAmE;IACnE,KAAK,EAAE,sBAAsB,CAAC;IAC9B,+EAA+E;IAC/E,cAAc,EAAE,gCAAgC,CAAC;IACjD,wDAAwD;IACxD,SAAS,EAAE,2BAA2B,CAAC;IACvC,2EAA2E;IAC3E,QAAQ,EAAE,0BAA0B,CAAC;IAErC,oEAAoE;IACpE,OAAO,CAAC,UAAU,CAAuB;IACzC,kDAAkD;IAClD,OAAO,CAAC,WAAW,CAAS;;IAY5B;;;;;;;OAOG;IACG,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../../src/providers/claude-code/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,OAAO,KAAK,EACV,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,gCAAgC,EAAE,MAAM,sBAAsB,CAAC;AACxE,OAAO,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAC;AAC5D,OAAO,EAAE,2BAA2B,EAAE,MAAM,gBAAgB,CAAC;AAI7D;;;;;;;;;;;;;;GAcG;AACH,qBAAa,iBAAkB,YAAW,mBAAmB;IAC3D,kCAAkC;IAClC,QAAQ,CAAC,EAAE,iBAAiB;IAC5B,oCAAoC;IACpC,QAAQ,CAAC,IAAI,iBAAiB;IAC9B,8BAA8B;IAC9B,QAAQ,CAAC,OAAO,WAAW;IAE3B,+CAA+C;IAC/C,YAAY,EAAE,mBAAmB,CA6B/B;IAEF,8DAA8D;IAC9D,KAAK,EAAE,sBAAsB,CAAC;IAC9B,wEAAwE;IACxE,KAAK,EAAE,uBAAuB,CAAC;IAC/B,+EAA+E;IAC/E,OAAO,EAAE,yBAAyB,CAAC;IACnC,mEAAmE;IACnE,KAAK,EAAE,sBAAsB,CAAC;IAC9B,+EAA+E;IAC/E,cAAc,EAAE,gCAAgC,CAAC;IACjD,wDAAwD;IACxD,SAAS,EAAE,2BAA2B,CAAC;IACvC,2EAA2E;IAC3E,QAAQ,EAAE,0BAA0B,CAAC;IAErC,oEAAoE;IACpE,OAAO,CAAC,UAAU,CAAuB;IACzC,kDAAkD;IAClD,OAAO,CAAC,WAAW,CAAS;;IAY5B;;;;;;;OAOG;IACG,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IASnD;;;;OAIG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ9B;;;;;;;;;OASG;IACG,WAAW,IAAI,OAAO,CAAC,mBAAmB,CAAC;IAyCjD;;OAEG;IACH,aAAa,IAAI,OAAO;IAIxB;;OAEG;IACH,aAAa,IAAI,MAAM,GAAG,IAAI;CAG/B"}
|
|
@@ -55,33 +55,45 @@ export declare class ClaudeCodeHookProvider implements AdapterHookProvider {
|
|
|
55
55
|
* @task T164
|
|
56
56
|
*/
|
|
57
57
|
mapProviderEvent(providerEvent: string): string | null;
|
|
58
|
+
/** Project directory this hook provider was registered for. */
|
|
59
|
+
private projectDir;
|
|
58
60
|
/**
|
|
59
61
|
* Register native hooks for a project.
|
|
60
62
|
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
63
|
+
* Writes CLEO hook entries to `~/.claude/settings.json` so that Claude Code's
|
|
64
|
+
* native event system calls cleo CLI commands when events fire. This bridges
|
|
65
|
+
* Claude Code's event loop to CLEO's internal hook dispatch.
|
|
64
66
|
*
|
|
65
|
-
*
|
|
66
|
-
* `getSupportedCanonicalEvents()` to enumerate all 14 supported hooks.
|
|
67
|
+
* Idempotent: skips writing if CLEO hooks already exist in settings.json.
|
|
67
68
|
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
69
|
+
* Hook entries registered:
|
|
70
|
+
* - `Stop` → `cleo session end --quiet` (triggers LLM extraction, reflector, consolidation)
|
|
71
|
+
* - `PostToolUse` (Write|Edit) → brain observation for file modifications
|
|
72
|
+
* - `SubagentStop` → brain observation for agent completion
|
|
73
|
+
*
|
|
74
|
+
* @param projectDir - Project directory for context-scoped hook commands
|
|
75
|
+
* @task T164 @task T555
|
|
70
76
|
*/
|
|
71
|
-
registerNativeHooks(
|
|
77
|
+
registerNativeHooks(projectDir: string): Promise<void>;
|
|
72
78
|
/**
|
|
73
79
|
* Unregister native hooks.
|
|
74
80
|
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
81
|
+
* Removes CLEO hook entries from `~/.claude/settings.json` by filtering out
|
|
82
|
+
* entries containing the `# cleo-hook` marker.
|
|
77
83
|
*
|
|
78
|
-
* @task T164
|
|
84
|
+
* @task T164 @task T555
|
|
79
85
|
*/
|
|
80
86
|
unregisterNativeHooks(): Promise<void>;
|
|
81
87
|
/**
|
|
82
88
|
* Check whether hooks have been registered via `registerNativeHooks`.
|
|
83
89
|
*/
|
|
84
90
|
isRegistered(): boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Get the project directory this hook provider was registered for.
|
|
93
|
+
*
|
|
94
|
+
* Returns null if hooks have not been registered yet.
|
|
95
|
+
*/
|
|
96
|
+
getProjectDir(): string | null;
|
|
85
97
|
/**
|
|
86
98
|
* Get the native→canonical event mapping for introspection and debugging.
|
|
87
99
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../../src/providers/claude-code/hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;
|
|
1
|
+
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../../src/providers/claude-code/hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AA8C/D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,sBAAuB,YAAW,mBAAmB;IAChE,kEAAkE;IAClE,OAAO,CAAC,UAAU,CAAS;IAE3B;;;;;;;;;;;OAWG;IACH,gBAAgB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAItD,+DAA+D;IAC/D,OAAO,CAAC,UAAU,CAAuB;IAEzC;;;;;;;;;;;;;;;;OAgBG;IACG,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAuE5D;;;;;;;OAOG;IACG,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IA2C5C;;OAEG;IACH,YAAY,IAAI,OAAO;IAIvB;;;;OAIG;IACH,aAAa,IAAI,MAAM,GAAG,IAAI;IAI9B;;;;;;;;OAQG;IACH,WAAW,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAI/C;;;;;;;;;;OAUG;IACG,2BAA2B,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAStD;;;;;;;;;;OAUG;IACG,kBAAkB,IAAI,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IASnD;;;;;;;;;;OAUG;IACG,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAW9D;;;;;;;;;;;;OAYG;IACG,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;CA2DrF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spawn.d.ts","sourceRoot":"","sources":["../../../src/providers/claude-code/spawn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,KAAK,EAAE,oBAAoB,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAY3F;;;;;;;;;;;;;;GAcG;AACH,qBAAa,uBAAwB,YAAW,oBAAoB;IAClE,mDAAmD;IACnD,OAAO,CAAC,UAAU,CAAqC;IAEvD;;;;OAIG;IACG,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC;IASlC;;;;;;;;OAQG;IACG,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"spawn.d.ts","sourceRoot":"","sources":["../../../src/providers/claude-code/spawn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,KAAK,EAAE,oBAAoB,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAY3F;;;;;;;;;;;;;;GAcG;AACH,qBAAa,uBAAwB,YAAW,oBAAoB;IAClE,mDAAmD;IACnD,OAAO,CAAC,UAAU,CAAqC;IAEvD;;;;OAIG;IACG,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC;IASlC;;;;;;;;OAQG;IACG,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC;IAqFxD;;;;;;;OAOG;IACG,WAAW,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IAqB3C;;;;;;;OAOG;IACG,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAWnD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spawn.d.ts","sourceRoot":"","sources":["../../../src/providers/opencode/spawn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,OAAO,KAAK,EAAE,oBAAoB,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAiB3F;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,0BAA0B,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAY5F;
|
|
1
|
+
{"version":3,"file":"spawn.d.ts","sourceRoot":"","sources":["../../../src/providers/opencode/spawn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,OAAO,KAAK,EAAE,oBAAoB,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAiB3F;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,0BAA0B,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAY5F;AA+CD;;;;;;;;;;;;;;GAcG;AACH,qBAAa,qBAAsB,YAAW,oBAAoB;IAChE,mDAAmD;IACnD,OAAO,CAAC,UAAU,CAAqC;IAEvD;;;;OAIG;IACG,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC;IASlC;;;;;;;;;OASG;IACG,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC;IA+ExD;;;;;;;OAOG;IACG,WAAW,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IAqB3C;;;;;;;OAOG;IACG,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAWnD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cleocode/adapters",
|
|
3
|
-
"version": "2026.4.
|
|
3
|
+
"version": "2026.4.40",
|
|
4
4
|
"description": "Unified provider adapters for CLEO (Claude Code, OpenCode, Cursor)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@cleocode/caamp": "2026.4.
|
|
16
|
-
"@cleocode/contracts": "2026.4.
|
|
15
|
+
"@cleocode/caamp": "2026.4.40",
|
|
16
|
+
"@cleocode/contracts": "2026.4.40"
|
|
17
17
|
},
|
|
18
18
|
"license": "MIT",
|
|
19
19
|
"engines": {
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the shared CANT context builder.
|
|
3
|
+
*
|
|
4
|
+
* Tests cover:
|
|
5
|
+
* - discoverCantFiles: finds .cant files, handles missing dirs
|
|
6
|
+
* - resolveThreeTierPaths: XDG-compliant paths with env var overrides
|
|
7
|
+
* - discoverCantFilesMultiTier: 3-tier merge with override semantics
|
|
8
|
+
* - readMemoryBridge: reads file, handles missing/empty
|
|
9
|
+
* - buildMemoryBridgeBlock: wraps content in labeled section
|
|
10
|
+
* - buildMentalModelInjection: pure function, numbered list, empty input
|
|
11
|
+
* - buildCantEnrichedPrompt: full pipeline, fallback on failure
|
|
12
|
+
*
|
|
13
|
+
* @task T555
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import { tmpdir } from 'node:os';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
buildCantEnrichedPrompt,
|
|
23
|
+
buildMemoryBridgeBlock,
|
|
24
|
+
buildMentalModelInjection,
|
|
25
|
+
discoverCantFiles,
|
|
26
|
+
discoverCantFilesMultiTier,
|
|
27
|
+
readMemoryBridge,
|
|
28
|
+
resolveThreeTierPaths,
|
|
29
|
+
} from '../cant-context.js';
|
|
30
|
+
|
|
31
|
+
let tempDir: string;
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
tempDir = join(tmpdir(), `cleo-cant-ctx-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`);
|
|
35
|
+
mkdirSync(tempDir, { recursive: true });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// discoverCantFiles
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
describe('discoverCantFiles', () => {
|
|
47
|
+
it('finds .cant files recursively', () => {
|
|
48
|
+
const cantDir = join(tempDir, 'cant');
|
|
49
|
+
mkdirSync(join(cantDir, 'agents'), { recursive: true });
|
|
50
|
+
writeFileSync(join(cantDir, 'team.cant'), 'team: test');
|
|
51
|
+
writeFileSync(join(cantDir, 'agents', 'worker.cant'), 'agent: worker');
|
|
52
|
+
writeFileSync(join(cantDir, 'agents', 'README.md'), 'ignored');
|
|
53
|
+
|
|
54
|
+
const files = discoverCantFiles(cantDir);
|
|
55
|
+
expect(files).toHaveLength(2);
|
|
56
|
+
expect(files.some((f) => f.endsWith('team.cant'))).toBe(true);
|
|
57
|
+
expect(files.some((f) => f.endsWith('worker.cant'))).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('returns empty array for non-existent directory', () => {
|
|
61
|
+
const files = discoverCantFiles(join(tempDir, 'does-not-exist'));
|
|
62
|
+
expect(files).toEqual([]);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// resolveThreeTierPaths
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
describe('resolveThreeTierPaths', () => {
|
|
71
|
+
it('returns project tier pointing to .cleo/cant/', () => {
|
|
72
|
+
const paths = resolveThreeTierPaths('/my/project');
|
|
73
|
+
expect(paths.project).toBe('/my/project/.cleo/cant');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('respects XDG_DATA_HOME for global tier', () => {
|
|
77
|
+
const originalXdg = process.env['XDG_DATA_HOME'];
|
|
78
|
+
process.env['XDG_DATA_HOME'] = '/custom/data';
|
|
79
|
+
try {
|
|
80
|
+
const paths = resolveThreeTierPaths('/my/project');
|
|
81
|
+
expect(paths.global).toBe('/custom/data/cleo/cant');
|
|
82
|
+
} finally {
|
|
83
|
+
if (originalXdg) process.env['XDG_DATA_HOME'] = originalXdg;
|
|
84
|
+
else delete process.env['XDG_DATA_HOME'];
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('respects XDG_CONFIG_HOME for user tier', () => {
|
|
89
|
+
const originalXdg = process.env['XDG_CONFIG_HOME'];
|
|
90
|
+
process.env['XDG_CONFIG_HOME'] = '/custom/config';
|
|
91
|
+
try {
|
|
92
|
+
const paths = resolveThreeTierPaths('/my/project');
|
|
93
|
+
expect(paths.user).toBe('/custom/config/cleo/cant');
|
|
94
|
+
} finally {
|
|
95
|
+
if (originalXdg) process.env['XDG_CONFIG_HOME'] = originalXdg;
|
|
96
|
+
else delete process.env['XDG_CONFIG_HOME'];
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// discoverCantFilesMultiTier
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
describe('discoverCantFilesMultiTier', () => {
|
|
106
|
+
let origXdgData: string | undefined;
|
|
107
|
+
let origXdgConfig: string | undefined;
|
|
108
|
+
|
|
109
|
+
beforeEach(() => {
|
|
110
|
+
// Override XDG paths so global/user tiers point to empty temp subdirs
|
|
111
|
+
origXdgData = process.env['XDG_DATA_HOME'];
|
|
112
|
+
origXdgConfig = process.env['XDG_CONFIG_HOME'];
|
|
113
|
+
process.env['XDG_DATA_HOME'] = join(tempDir, 'xdg-data');
|
|
114
|
+
process.env['XDG_CONFIG_HOME'] = join(tempDir, 'xdg-config');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
afterEach(() => {
|
|
118
|
+
if (origXdgData) process.env['XDG_DATA_HOME'] = origXdgData;
|
|
119
|
+
else delete process.env['XDG_DATA_HOME'];
|
|
120
|
+
if (origXdgConfig) process.env['XDG_CONFIG_HOME'] = origXdgConfig;
|
|
121
|
+
else delete process.env['XDG_CONFIG_HOME'];
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('discovers files from project tier', () => {
|
|
125
|
+
const cantDir = join(tempDir, '.cleo', 'cant');
|
|
126
|
+
mkdirSync(cantDir, { recursive: true });
|
|
127
|
+
writeFileSync(join(cantDir, 'team.cant'), 'team: test');
|
|
128
|
+
|
|
129
|
+
const result = discoverCantFilesMultiTier(tempDir);
|
|
130
|
+
expect(result.files).toHaveLength(1);
|
|
131
|
+
expect(result.stats.project).toBe(1);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('returns empty when no tiers have .cant files', () => {
|
|
135
|
+
const result = discoverCantFilesMultiTier(tempDir);
|
|
136
|
+
expect(result.files).toHaveLength(0);
|
|
137
|
+
expect(result.stats.merged).toBe(0);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// readMemoryBridge
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
describe('readMemoryBridge', () => {
|
|
146
|
+
it('returns null when file does not exist', () => {
|
|
147
|
+
expect(readMemoryBridge(tempDir)).toBeNull();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('returns content when file exists', () => {
|
|
151
|
+
const cleoDir = join(tempDir, '.cleo');
|
|
152
|
+
mkdirSync(cleoDir, { recursive: true });
|
|
153
|
+
writeFileSync(join(cleoDir, 'memory-bridge.md'), '# Memory Bridge\nTest content');
|
|
154
|
+
|
|
155
|
+
const result = readMemoryBridge(tempDir);
|
|
156
|
+
expect(result).toContain('Memory Bridge');
|
|
157
|
+
expect(result).toContain('Test content');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('returns null for empty file', () => {
|
|
161
|
+
const cleoDir = join(tempDir, '.cleo');
|
|
162
|
+
mkdirSync(cleoDir, { recursive: true });
|
|
163
|
+
writeFileSync(join(cleoDir, 'memory-bridge.md'), '');
|
|
164
|
+
|
|
165
|
+
expect(readMemoryBridge(tempDir)).toBeNull();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// buildMemoryBridgeBlock
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
describe('buildMemoryBridgeBlock', () => {
|
|
174
|
+
it('wraps content in labeled section markers', () => {
|
|
175
|
+
const result = buildMemoryBridgeBlock('Test content');
|
|
176
|
+
expect(result).toContain('===== CLEO MEMORY BRIDGE =====');
|
|
177
|
+
expect(result).toContain('Test content');
|
|
178
|
+
expect(result).toContain('===== END MEMORY BRIDGE =====');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// buildMentalModelInjection
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
describe('buildMentalModelInjection', () => {
|
|
187
|
+
it('returns empty string for empty observations', () => {
|
|
188
|
+
expect(buildMentalModelInjection('test-agent', [])).toBe('');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('builds numbered list with preamble', () => {
|
|
192
|
+
const result = buildMentalModelInjection('code-worker', [
|
|
193
|
+
{ id: 'O-001', type: 'observation', title: 'Tests pass', date: '2026-04-14' },
|
|
194
|
+
{ id: 'O-002', type: 'pattern', title: 'Use vitest' },
|
|
195
|
+
]);
|
|
196
|
+
|
|
197
|
+
expect(result).toContain('MENTAL MODEL (validate-on-load)');
|
|
198
|
+
expect(result).toContain('Agent: code-worker');
|
|
199
|
+
expect(result).toContain('1. [O-001] (observation) [2026-04-14]: Tests pass');
|
|
200
|
+
expect(result).toContain('2. [O-002] (pattern): Use vitest');
|
|
201
|
+
expect(result).toContain('END MENTAL MODEL');
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// buildCantEnrichedPrompt
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
describe('buildCantEnrichedPrompt', () => {
|
|
210
|
+
it('returns basePrompt unchanged when no .cant files exist', async () => {
|
|
211
|
+
const result = await buildCantEnrichedPrompt({
|
|
212
|
+
projectDir: tempDir,
|
|
213
|
+
basePrompt: 'Execute the task.',
|
|
214
|
+
});
|
|
215
|
+
expect(result).toBe('Execute the task.');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('appends memory bridge when .cleo/memory-bridge.md exists', async () => {
|
|
219
|
+
const cleoDir = join(tempDir, '.cleo');
|
|
220
|
+
mkdirSync(cleoDir, { recursive: true });
|
|
221
|
+
writeFileSync(join(cleoDir, 'memory-bridge.md'), '# Bridge\nRecent decisions here');
|
|
222
|
+
|
|
223
|
+
const result = await buildCantEnrichedPrompt({
|
|
224
|
+
projectDir: tempDir,
|
|
225
|
+
basePrompt: 'Execute the task.',
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
expect(result).toContain('Execute the task.');
|
|
229
|
+
expect(result).toContain('CLEO MEMORY BRIDGE');
|
|
230
|
+
expect(result).toContain('Recent decisions here');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('includes both memory bridge and base prompt without duplication', async () => {
|
|
234
|
+
const cleoDir = join(tempDir, '.cleo');
|
|
235
|
+
mkdirSync(cleoDir, { recursive: true });
|
|
236
|
+
writeFileSync(join(cleoDir, 'memory-bridge.md'), 'Bridge content');
|
|
237
|
+
|
|
238
|
+
const result = await buildCantEnrichedPrompt({
|
|
239
|
+
projectDir: tempDir,
|
|
240
|
+
basePrompt: 'My prompt',
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Base prompt should appear exactly once at the start
|
|
244
|
+
expect(result.startsWith('My prompt')).toBe(true);
|
|
245
|
+
// Should not duplicate the prompt
|
|
246
|
+
expect(result.indexOf('My prompt')).toBe(result.lastIndexOf('My prompt'));
|
|
247
|
+
});
|
|
248
|
+
});
|