create-walle 0.1.0
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/bin/create-walle.js +134 -0
- package/package.json +18 -0
- package/template/.env.example +40 -0
- package/template/CLAUDE.md +12 -0
- package/template/LICENSE +21 -0
- package/template/README.md +167 -0
- package/template/bin/setup.js +100 -0
- package/template/claude-code-skill.md +60 -0
- package/template/claude-task-manager/api-prompts.js +1841 -0
- package/template/claude-task-manager/api-reviews.js +275 -0
- package/template/claude-task-manager/approval-agent.js +454 -0
- package/template/claude-task-manager/bin/restart-ctm.sh +16 -0
- package/template/claude-task-manager/db.js +1721 -0
- package/template/claude-task-manager/docs/PROMPT-MANAGEMENT-DESIGN.md +631 -0
- package/template/claude-task-manager/git-utils.js +214 -0
- package/template/claude-task-manager/package-lock.json +1607 -0
- package/template/claude-task-manager/package.json +31 -0
- package/template/claude-task-manager/prompt-harvest.js +1148 -0
- package/template/claude-task-manager/public/css/prompts.css +880 -0
- package/template/claude-task-manager/public/css/reviews.css +430 -0
- package/template/claude-task-manager/public/css/walle.css +732 -0
- package/template/claude-task-manager/public/favicon.ico +0 -0
- package/template/claude-task-manager/public/icon.svg +37 -0
- package/template/claude-task-manager/public/index.html +8346 -0
- package/template/claude-task-manager/public/js/prompts.js +3159 -0
- package/template/claude-task-manager/public/js/reviews.js +1292 -0
- package/template/claude-task-manager/public/js/walle.js +3081 -0
- package/template/claude-task-manager/public/manifest.json +13 -0
- package/template/claude-task-manager/public/prompts.html +4353 -0
- package/template/claude-task-manager/public/setup.html +216 -0
- package/template/claude-task-manager/queue-engine.js +404 -0
- package/template/claude-task-manager/server-state.js +5 -0
- package/template/claude-task-manager/server.js +2254 -0
- package/template/claude-task-manager/session-utils.js +124 -0
- package/template/claude-task-manager/start.sh +17 -0
- package/template/claude-task-manager/tests/test-ai-search.js +61 -0
- package/template/claude-task-manager/tests/test-editor-ux.js +76 -0
- package/template/claude-task-manager/tests/test-editor-ux2.js +51 -0
- package/template/claude-task-manager/tests/test-features-v2.js +127 -0
- package/template/claude-task-manager/tests/test-insights-cached.js +78 -0
- package/template/claude-task-manager/tests/test-insights.js +124 -0
- package/template/claude-task-manager/tests/test-permissions-v2.js +127 -0
- package/template/claude-task-manager/tests/test-permissions.js +122 -0
- package/template/claude-task-manager/tests/test-pin.js +51 -0
- package/template/claude-task-manager/tests/test-prompts.js +164 -0
- package/template/claude-task-manager/tests/test-recent-sessions.js +96 -0
- package/template/claude-task-manager/tests/test-review.js +104 -0
- package/template/claude-task-manager/tests/test-send-dropdown.js +76 -0
- package/template/claude-task-manager/tests/test-send-final.js +30 -0
- package/template/claude-task-manager/tests/test-send-fixes.js +76 -0
- package/template/claude-task-manager/tests/test-send-integration.js +107 -0
- package/template/claude-task-manager/tests/test-send-visual.js +34 -0
- package/template/claude-task-manager/tests/test-session-create.js +147 -0
- package/template/claude-task-manager/tests/test-sidebar-ux.js +83 -0
- package/template/claude-task-manager/tests/test-url-hash.js +68 -0
- package/template/claude-task-manager/tests/test-ux-crop.js +34 -0
- package/template/claude-task-manager/tests/test-ux-review.js +130 -0
- package/template/claude-task-manager/tests/test-zoom-card.js +76 -0
- package/template/claude-task-manager/tests/test-zoom.js +92 -0
- package/template/claude-task-manager/tests/test-zoom2.js +67 -0
- package/template/docs/site/api/README.md +187 -0
- package/template/docs/site/guides/claude-code.md +58 -0
- package/template/docs/site/guides/configuration.md +96 -0
- package/template/docs/site/guides/quickstart.md +158 -0
- package/template/docs/site/index.md +14 -0
- package/template/docs/site/skills/README.md +135 -0
- package/template/wall-e/.dockerignore +11 -0
- package/template/wall-e/Dockerfile +25 -0
- package/template/wall-e/adapters/adapter-base.js +37 -0
- package/template/wall-e/adapters/ctm.js +193 -0
- package/template/wall-e/adapters/slack.js +56 -0
- package/template/wall-e/agent.js +319 -0
- package/template/wall-e/api-walle.js +1073 -0
- package/template/wall-e/brain.js +1235 -0
- package/template/wall-e/channels/agent-api.js +172 -0
- package/template/wall-e/channels/channel-base.js +14 -0
- package/template/wall-e/channels/imessage-channel.js +113 -0
- package/template/wall-e/channels/slack-channel.js +118 -0
- package/template/wall-e/chat.js +778 -0
- package/template/wall-e/decision/confidence.js +93 -0
- package/template/wall-e/deploy.sh +35 -0
- package/template/wall-e/docs/specs/2026-04-01-publish-plan.md +112 -0
- package/template/wall-e/docs/specs/SKILL-FORMAT.md +326 -0
- package/template/wall-e/extraction/contradiction.js +168 -0
- package/template/wall-e/extraction/knowledge-extractor.js +190 -0
- package/template/wall-e/fly.toml +24 -0
- package/template/wall-e/loops/ingest.js +34 -0
- package/template/wall-e/loops/reflect.js +63 -0
- package/template/wall-e/loops/tasks.js +487 -0
- package/template/wall-e/loops/think.js +125 -0
- package/template/wall-e/package-lock.json +533 -0
- package/template/wall-e/package.json +18 -0
- package/template/wall-e/scripts/ingest-slack-search.js +85 -0
- package/template/wall-e/scripts/pull-slack-via-claude.js +98 -0
- package/template/wall-e/scripts/slack-backfill.js +295 -0
- package/template/wall-e/scripts/slack-channel-history.js +454 -0
- package/template/wall-e/server.js +93 -0
- package/template/wall-e/skills/_bundled/email-digest/SKILL.md +95 -0
- package/template/wall-e/skills/_bundled/email-sync/SKILL.md +65 -0
- package/template/wall-e/skills/_bundled/email-sync/mail-reader.jxa +104 -0
- package/template/wall-e/skills/_bundled/email-sync/run.js +213 -0
- package/template/wall-e/skills/_bundled/google-calendar/SKILL.md +73 -0
- package/template/wall-e/skills/_bundled/google-calendar/cal-reader.swift +81 -0
- package/template/wall-e/skills/_bundled/google-calendar/run.js +181 -0
- package/template/wall-e/skills/_bundled/memory-search/SKILL.md +92 -0
- package/template/wall-e/skills/_bundled/morning-briefing/SKILL.md +131 -0
- package/template/wall-e/skills/_bundled/morning-briefing/run.js +264 -0
- package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +60 -0
- package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +55 -0
- package/template/wall-e/skills/claude-code-reader.js +144 -0
- package/template/wall-e/skills/mcp-client.js +407 -0
- package/template/wall-e/skills/skill-executor.js +163 -0
- package/template/wall-e/skills/skill-loader.js +410 -0
- package/template/wall-e/skills/skill-planner.js +88 -0
- package/template/wall-e/skills/slack-ingest.js +329 -0
- package/template/wall-e/skills/slack-pull-live.js +270 -0
- package/template/wall-e/skills/tool-executor.js +188 -0
- package/template/wall-e/tests/adapter-base.test.js +20 -0
- package/template/wall-e/tests/adapter-ctm.test.js +122 -0
- package/template/wall-e/tests/adapter-slack.test.js +98 -0
- package/template/wall-e/tests/agent-api.test.js +256 -0
- package/template/wall-e/tests/api-walle.test.js +222 -0
- package/template/wall-e/tests/brain.test.js +602 -0
- package/template/wall-e/tests/channels.test.js +104 -0
- package/template/wall-e/tests/chat.test.js +103 -0
- package/template/wall-e/tests/confidence.test.js +134 -0
- package/template/wall-e/tests/contradiction.test.js +217 -0
- package/template/wall-e/tests/ingest.test.js +113 -0
- package/template/wall-e/tests/mcp-client.test.js +71 -0
- package/template/wall-e/tests/reflect.test.js +103 -0
- package/template/wall-e/tests/server.test.js +111 -0
- package/template/wall-e/tests/skills.test.js +198 -0
- package/template/wall-e/tests/slack-ingest.test.js +103 -0
- package/template/wall-e/tests/think.test.js +435 -0
- package/template/wall-e/tools/local-tools.js +697 -0
- package/template/wall-e/tools/slack-mcp.js +290 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { describe, it } = require('node:test');
|
|
3
|
+
const assert = require('node:assert');
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
loadMcpConfigs,
|
|
7
|
+
StdioTransport,
|
|
8
|
+
HttpTransport,
|
|
9
|
+
getConnection,
|
|
10
|
+
disconnectAll,
|
|
11
|
+
listAllTools,
|
|
12
|
+
} = require('../skills/mcp-client');
|
|
13
|
+
|
|
14
|
+
describe('mcp-client', () => {
|
|
15
|
+
describe('loadMcpConfigs', () => {
|
|
16
|
+
it('returns an object with server names as keys', () => {
|
|
17
|
+
const configs = loadMcpConfigs();
|
|
18
|
+
assert.strictEqual(typeof configs, 'object');
|
|
19
|
+
assert.ok(configs !== null);
|
|
20
|
+
// Each value should have a name property
|
|
21
|
+
for (const [key, cfg] of Object.entries(configs)) {
|
|
22
|
+
assert.strictEqual(cfg.name, key);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('StdioTransport', () => {
|
|
28
|
+
it('stores config on construction', () => {
|
|
29
|
+
const config = { name: 'test-server', command: 'node', args: ['--version'] };
|
|
30
|
+
const transport = new StdioTransport(config);
|
|
31
|
+
assert.strictEqual(transport.config.name, 'test-server');
|
|
32
|
+
assert.strictEqual(transport.config.command, 'node');
|
|
33
|
+
assert.strictEqual(transport.initialized, false);
|
|
34
|
+
assert.strictEqual(transport.nextId, 1);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('HttpTransport', () => {
|
|
39
|
+
it('stores URL from config', () => {
|
|
40
|
+
const config = { name: 'http-server', url: 'https://example.com/mcp', type: 'http' };
|
|
41
|
+
const transport = new HttpTransport(config);
|
|
42
|
+
assert.strictEqual(transport.url, 'https://example.com/mcp');
|
|
43
|
+
assert.strictEqual(transport.initialized, false);
|
|
44
|
+
assert.strictEqual(transport.nextId, 1);
|
|
45
|
+
assert.deepStrictEqual(transport.authHeaders, {});
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('getConnection', () => {
|
|
50
|
+
it('throws for unknown server name', async () => {
|
|
51
|
+
await assert.rejects(
|
|
52
|
+
() => getConnection('nonexistent-server-xyz-999'),
|
|
53
|
+
/not found in config/
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('disconnectAll', () => {
|
|
59
|
+
it('clears connections without error', () => {
|
|
60
|
+
// Should not throw even when there are no connections
|
|
61
|
+
assert.doesNotThrow(() => disconnectAll());
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('listAllTools', () => {
|
|
66
|
+
it('returns an array (may be empty if no servers reachable)', async () => {
|
|
67
|
+
const tools = await listAllTools();
|
|
68
|
+
assert.ok(Array.isArray(tools));
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const { describe, it, before, after } = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const brain = require('../brain');
|
|
7
|
+
const reflect = require('../loops/reflect');
|
|
8
|
+
|
|
9
|
+
describe('reflect loop', () => {
|
|
10
|
+
const ownerName = 'ReflectOwner';
|
|
11
|
+
let tmpDir;
|
|
12
|
+
|
|
13
|
+
const mockGenerateFn = async (memories, stats) => {
|
|
14
|
+
return 'Daily summary: 10 memories ingested, 3 knowledge extracted.';
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
before(() => {
|
|
18
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'walle-reflect-test-'));
|
|
19
|
+
brain.initDb(path.join(tmpDir, 'test.db'));
|
|
20
|
+
brain.setOwner('name', ownerName);
|
|
21
|
+
|
|
22
|
+
// Insert some test memories
|
|
23
|
+
for (let i = 0; i < 5; i++) {
|
|
24
|
+
brain.insertMemory({
|
|
25
|
+
source: 'test',
|
|
26
|
+
memory_type: 'observation',
|
|
27
|
+
content: `Test memory ${i}`,
|
|
28
|
+
timestamp: new Date().toISOString(),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Insert a pending question
|
|
33
|
+
brain.insertQuestion({
|
|
34
|
+
question_type: 'preference',
|
|
35
|
+
question: 'Does user prefer tabs or spaces?',
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
after(() => {
|
|
40
|
+
brain.closeDb();
|
|
41
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('returns stats object with memoriesSinceLastReflect and pendingQuestions', async () => {
|
|
45
|
+
const result = await reflect.runOnce({});
|
|
46
|
+
assert.ok('memoriesSinceLastReflect' in result, 'should have memoriesSinceLastReflect');
|
|
47
|
+
assert.ok('pendingQuestions' in result, 'should have pendingQuestions');
|
|
48
|
+
assert.ok('summaryGenerated' in result, 'should have summaryGenerated');
|
|
49
|
+
assert.ok(result.memoriesSinceLastReflect >= 5, 'should count memories');
|
|
50
|
+
assert.equal(result.pendingQuestions, 1, 'should count pending questions');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('creates daily summary when forceSummary is true', async () => {
|
|
54
|
+
const result = await reflect.runOnce({ forceSummary: true, generateFn: mockGenerateFn });
|
|
55
|
+
assert.equal(result.summaryGenerated, true, 'summary should be generated');
|
|
56
|
+
|
|
57
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
58
|
+
const summary = brain.getDailySummary(today);
|
|
59
|
+
assert.ok(summary, 'summary should exist in DB');
|
|
60
|
+
assert.ok(summary.summary.includes('Daily summary'), 'summary text should match');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('generateDailySummary stores summary in DB and returns it', async () => {
|
|
64
|
+
// Use a different date by creating a fresh DB
|
|
65
|
+
const tmpDir2 = fs.mkdtempSync(path.join(os.tmpdir(), 'walle-reflect-gen-'));
|
|
66
|
+
brain.closeDb();
|
|
67
|
+
brain.initDb(path.join(tmpDir2, 'test.db'));
|
|
68
|
+
brain.setOwner('name', ownerName);
|
|
69
|
+
|
|
70
|
+
const result = await reflect.generateDailySummary({ generateFn: mockGenerateFn });
|
|
71
|
+
assert.ok(result, 'should return summary');
|
|
72
|
+
assert.ok(result.summary.includes('Daily summary'), 'summary text should match mock');
|
|
73
|
+
|
|
74
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
75
|
+
const stored = brain.getDailySummary(today);
|
|
76
|
+
assert.ok(stored, 'summary should be stored in DB');
|
|
77
|
+
assert.equal(stored.summary, result.summary);
|
|
78
|
+
|
|
79
|
+
brain.closeDb();
|
|
80
|
+
fs.rmSync(tmpDir2, { recursive: true, force: true });
|
|
81
|
+
|
|
82
|
+
// Re-init original DB for remaining tests
|
|
83
|
+
brain.initDb(path.join(tmpDir, 'test.db'));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('returns existing summary without creating duplicate for same day', async () => {
|
|
87
|
+
// First call should have already created today's summary from the forceSummary test
|
|
88
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
89
|
+
const existingBefore = brain.getDailySummary(today);
|
|
90
|
+
assert.ok(existingBefore, 'summary should already exist from previous test');
|
|
91
|
+
|
|
92
|
+
const result = await reflect.generateDailySummary({ generateFn: mockGenerateFn });
|
|
93
|
+
assert.equal(result.id, existingBefore.id, 'should return same summary, not create new one');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('updates checkpoint after reflect', async () => {
|
|
97
|
+
await reflect.runOnce({});
|
|
98
|
+
const checkpoint = brain.getCheckpoint('reflect');
|
|
99
|
+
assert.ok(checkpoint, 'checkpoint should exist');
|
|
100
|
+
assert.ok(checkpoint.last_run_at, 'checkpoint should have last_run_at');
|
|
101
|
+
assert.ok(checkpoint.metadata, 'checkpoint should have metadata');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { describe, it, before, after } = require('node:test');
|
|
3
|
+
const assert = require('node:assert');
|
|
4
|
+
const http = require('http');
|
|
5
|
+
|
|
6
|
+
// Use a random high port for testing
|
|
7
|
+
const TEST_PORT = 19000 + Math.floor(Math.random() * 1000);
|
|
8
|
+
process.env.WALL_E_PORT = String(TEST_PORT);
|
|
9
|
+
process.env.WALL_E_HOST = '127.0.0.1';
|
|
10
|
+
|
|
11
|
+
// We need brain initialized for status endpoint — use a temp db
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'walle-server-test-'));
|
|
16
|
+
process.env.WALL_E_DATA_DIR = tmpDir;
|
|
17
|
+
|
|
18
|
+
const brain = require('../brain');
|
|
19
|
+
const { _setReadDb, _setBrain } = require('../api-walle');
|
|
20
|
+
|
|
21
|
+
let server;
|
|
22
|
+
|
|
23
|
+
function request(urlPath, options = {}) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const req = http.request({
|
|
26
|
+
hostname: '127.0.0.1',
|
|
27
|
+
port: TEST_PORT,
|
|
28
|
+
path: urlPath,
|
|
29
|
+
method: options.method || 'GET',
|
|
30
|
+
headers: options.headers || {},
|
|
31
|
+
}, (res) => {
|
|
32
|
+
const chunks = [];
|
|
33
|
+
res.on('data', chunk => chunks.push(chunk));
|
|
34
|
+
res.on('end', () => {
|
|
35
|
+
const body = Buffer.concat(chunks).toString();
|
|
36
|
+
let json = null;
|
|
37
|
+
try { json = JSON.parse(body); } catch {}
|
|
38
|
+
resolve({ status: res.statusCode, headers: res.headers, body, json });
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
req.on('error', reject);
|
|
42
|
+
if (options.body) req.write(JSON.stringify(options.body));
|
|
43
|
+
req.end();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('WALL-E HTTP Server', () => {
|
|
48
|
+
before(async () => {
|
|
49
|
+
brain.initDb(path.join(tmpDir, 'test-brain.db'));
|
|
50
|
+
_setBrain(brain);
|
|
51
|
+
|
|
52
|
+
// Open a read-only connection for api-walle
|
|
53
|
+
const Database = require('better-sqlite3');
|
|
54
|
+
const readDb = new Database(path.join(tmpDir, 'test-brain.db'), { readonly: true });
|
|
55
|
+
readDb.pragma('journal_mode = WAL');
|
|
56
|
+
_setReadDb(readDb);
|
|
57
|
+
|
|
58
|
+
const { startServer } = require('../server');
|
|
59
|
+
server = startServer();
|
|
60
|
+
|
|
61
|
+
// Wait for server to be listening
|
|
62
|
+
await new Promise((resolve) => {
|
|
63
|
+
if (server.listening) return resolve();
|
|
64
|
+
server.on('listening', resolve);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
after(() => {
|
|
69
|
+
if (server) server.close();
|
|
70
|
+
brain.closeDb();
|
|
71
|
+
// Cleanup temp dir
|
|
72
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('GET /api/wall-e/health returns {status: "ok"}', async () => {
|
|
76
|
+
const res = await request('/api/wall-e/health');
|
|
77
|
+
assert.strictEqual(res.status, 200);
|
|
78
|
+
assert.strictEqual(res.json.status, 'ok');
|
|
79
|
+
assert.strictEqual(res.json.version, '0.1.0');
|
|
80
|
+
assert.strictEqual(typeof res.json.uptime, 'number');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('GET /api/wall-e/status returns enriched status', async () => {
|
|
84
|
+
const res = await request('/api/wall-e/status');
|
|
85
|
+
assert.strictEqual(res.status, 200);
|
|
86
|
+
assert.ok(res.json.data);
|
|
87
|
+
assert.strictEqual(res.json.data.version, '0.1.0');
|
|
88
|
+
assert.strictEqual(typeof res.json.data.uptime, 'number');
|
|
89
|
+
assert.ok(res.json.data.stats);
|
|
90
|
+
assert.ok('loop_health' in res.json.data);
|
|
91
|
+
assert.ok('adapters' in res.json.data);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('GET /unknown returns 404', async () => {
|
|
95
|
+
const res = await request('/unknown');
|
|
96
|
+
assert.strictEqual(res.status, 404);
|
|
97
|
+
assert.strictEqual(res.json.error, 'Not found');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('GET /api/wall-e/nonexistent returns 404 from API handler', async () => {
|
|
101
|
+
const res = await request('/api/wall-e/nonexistent');
|
|
102
|
+
assert.strictEqual(res.status, 404);
|
|
103
|
+
assert.strictEqual(res.json.error, 'Not found');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('OPTIONS request returns 204 with CORS headers', async () => {
|
|
107
|
+
const res = await request('/api/wall-e/health', { method: 'OPTIONS' });
|
|
108
|
+
assert.strictEqual(res.status, 204);
|
|
109
|
+
assert.ok(res.headers['access-control-allow-origin']);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { describe, it, before, after } = require('node:test');
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
const brain = require('../brain.js');
|
|
9
|
+
|
|
10
|
+
let testDir;
|
|
11
|
+
let testDbPath;
|
|
12
|
+
|
|
13
|
+
function setupTestDb() {
|
|
14
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wall-e-skills-test-'));
|
|
15
|
+
testDbPath = path.join(testDir, 'test-brain.db');
|
|
16
|
+
brain.initDb(testDbPath);
|
|
17
|
+
brain.setOwner('name', 'Test User');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function teardownTestDb() {
|
|
21
|
+
try { brain.closeDb(); } catch (_) {}
|
|
22
|
+
if (testDir && fs.existsSync(testDir)) {
|
|
23
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ── Brain Skills CRUD ────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
describe('Skills CRUD', () => {
|
|
30
|
+
before(() => setupTestDb());
|
|
31
|
+
after(() => teardownTestDb());
|
|
32
|
+
|
|
33
|
+
it('insertSkill creates a skill and returns id', () => {
|
|
34
|
+
const result = brain.insertSkill({
|
|
35
|
+
name: 'test-skill',
|
|
36
|
+
description: 'A test skill',
|
|
37
|
+
trigger_type: 'interval',
|
|
38
|
+
trigger_config: JSON.stringify({ interval_ms: 60000 }),
|
|
39
|
+
prompt_template: 'Do something',
|
|
40
|
+
});
|
|
41
|
+
assert.ok(result.id, 'should return an id');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('getSkill retrieves the created skill', () => {
|
|
45
|
+
const skill = brain.getSkillByName('test-skill');
|
|
46
|
+
assert.ok(skill);
|
|
47
|
+
assert.equal(skill.name, 'test-skill');
|
|
48
|
+
assert.equal(skill.description, 'A test skill');
|
|
49
|
+
assert.equal(skill.enabled, 1);
|
|
50
|
+
assert.equal(skill.success_count, 0);
|
|
51
|
+
assert.equal(skill.failure_count, 0);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('listSkills returns all skills', () => {
|
|
55
|
+
const skills = brain.listSkills({});
|
|
56
|
+
assert.ok(skills.length >= 1);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('listSkills filters by enabled', () => {
|
|
60
|
+
brain.insertSkill({ name: 'disabled-skill', description: 'disabled' });
|
|
61
|
+
const skill = brain.getSkillByName('disabled-skill');
|
|
62
|
+
brain.updateSkill(skill.id, { enabled: 0 });
|
|
63
|
+
|
|
64
|
+
const enabled = brain.listSkills({ enabled: 1 });
|
|
65
|
+
const disabled = brain.listSkills({ enabled: 0 });
|
|
66
|
+
assert.ok(enabled.every(s => s.enabled === 1));
|
|
67
|
+
assert.ok(disabled.every(s => s.enabled === 0));
|
|
68
|
+
assert.ok(disabled.length >= 1);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('updateSkillStats increments success_count', () => {
|
|
72
|
+
const skill = brain.getSkillByName('test-skill');
|
|
73
|
+
brain.updateSkillStats(skill.id, true);
|
|
74
|
+
const updated = brain.getSkill(skill.id);
|
|
75
|
+
assert.equal(updated.success_count, 1);
|
|
76
|
+
assert.equal(updated.last_result, 'success');
|
|
77
|
+
assert.ok(updated.last_run);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('updateSkillStats increments failure_count', () => {
|
|
81
|
+
const skill = brain.getSkillByName('test-skill');
|
|
82
|
+
brain.updateSkillStats(skill.id, false);
|
|
83
|
+
const updated = brain.getSkill(skill.id);
|
|
84
|
+
assert.equal(updated.failure_count, 1);
|
|
85
|
+
assert.equal(updated.last_result, 'failure');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('insertSkillExecution creates an execution record', () => {
|
|
89
|
+
const skill = brain.getSkillByName('test-skill');
|
|
90
|
+
const result = brain.insertSkillExecution({
|
|
91
|
+
skill_id: skill.id,
|
|
92
|
+
status: 'success',
|
|
93
|
+
tool_calls: '[]',
|
|
94
|
+
tool_results: '[]',
|
|
95
|
+
memories_created: 3,
|
|
96
|
+
duration_ms: 1500,
|
|
97
|
+
});
|
|
98
|
+
assert.ok(result.id);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('listSkillExecutions returns executions for a skill', () => {
|
|
102
|
+
const skill = brain.getSkillByName('test-skill');
|
|
103
|
+
const execs = brain.listSkillExecutions({ skill_id: skill.id, limit: 10 });
|
|
104
|
+
assert.ok(execs.length >= 1);
|
|
105
|
+
assert.equal(execs[0].status, 'success');
|
|
106
|
+
assert.equal(execs[0].memories_created, 3);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('insertSkill with duplicate name throws', () => {
|
|
110
|
+
assert.throws(() => brain.insertSkill({ name: 'test-skill', description: 'dup' }));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('updateSkill rejects invalid fields', () => {
|
|
114
|
+
const skill = brain.getSkillByName('test-skill');
|
|
115
|
+
assert.throws(() => brain.updateSkill(skill.id, { id: 'evil' }), /Invalid update fields/);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ── Tool Executor ────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
describe('Tool Executor', () => {
|
|
122
|
+
const { executeTool, getToolDefinitions } = require('../skills/tool-executor');
|
|
123
|
+
|
|
124
|
+
it('getToolDefinitions returns array of tools', () => {
|
|
125
|
+
const defs = getToolDefinitions();
|
|
126
|
+
assert.ok(defs.length >= 3, `Expected at least 3 tools, got ${defs.length}`);
|
|
127
|
+
const names = defs.map(d => d.name);
|
|
128
|
+
assert.ok(names.includes('http_fetch'));
|
|
129
|
+
assert.ok(names.includes('read_file'));
|
|
130
|
+
assert.ok(names.includes('shell_exec'));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('executeTool returns error for unknown tool', async () => {
|
|
134
|
+
const result = await executeTool('nonexistent', {});
|
|
135
|
+
assert.equal(result.success, false);
|
|
136
|
+
assert.ok(result.error.includes('Unknown tool'));
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('executeTool read_file returns error for nonexistent file', async () => {
|
|
140
|
+
const result = await executeTool('read_file', { file_path: '/nonexistent/path/file.txt' });
|
|
141
|
+
assert.equal(result.success, false);
|
|
142
|
+
assert.ok(result.error);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('executeTool shell_exec with echo returns stdout', async () => {
|
|
146
|
+
const result = await executeTool('shell_exec', { command: 'echo', args: ['hello'] });
|
|
147
|
+
assert.equal(result.success, true);
|
|
148
|
+
assert.ok(result.result.stdout.includes('hello'));
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('executeTool shell_exec rejects disallowed command', async () => {
|
|
152
|
+
const result = await executeTool('shell_exec', { command: 'rm', args: ['-rf', '/'] });
|
|
153
|
+
assert.equal(result.success, false);
|
|
154
|
+
assert.ok(result.error.includes('not allowed'));
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('executeTool shell_exec rejects command without name', async () => {
|
|
158
|
+
const result = await executeTool('shell_exec', { command: '' });
|
|
159
|
+
assert.equal(result.success, false);
|
|
160
|
+
assert.ok(result.error.includes('required'));
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('executeTool http_fetch rejects non-http URLs', async () => {
|
|
164
|
+
const result = await executeTool('http_fetch', { url: 'file:///etc/passwd' });
|
|
165
|
+
assert.equal(result.success, false);
|
|
166
|
+
assert.ok(result.error.includes('Only http/https'));
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('executeTool read_file rejects paths outside home', async () => {
|
|
170
|
+
const result = await executeTool('read_file', { file_path: '/etc/passwd' });
|
|
171
|
+
assert.equal(result.success, false);
|
|
172
|
+
assert.ok(result.error.includes('home directory'));
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// ── Claude Code Reader ────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
describe('Claude Code Reader', () => {
|
|
179
|
+
const { readMcpConfig, readClaudeSkills, suggestSkillsFromClaudeCode } = require('../skills/claude-code-reader');
|
|
180
|
+
|
|
181
|
+
it('readMcpConfig returns an array', () => {
|
|
182
|
+
const configs = readMcpConfig();
|
|
183
|
+
assert.ok(Array.isArray(configs));
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('readClaudeSkills returns an array', () => {
|
|
187
|
+
const skills = readClaudeSkills();
|
|
188
|
+
assert.ok(Array.isArray(skills));
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('suggestSkillsFromClaudeCode returns object with expected keys', () => {
|
|
192
|
+
const result = suggestSkillsFromClaudeCode();
|
|
193
|
+
assert.ok(result);
|
|
194
|
+
assert.ok(Array.isArray(result.mcpServers));
|
|
195
|
+
assert.ok(Array.isArray(result.claudeSkills));
|
|
196
|
+
assert.ok(Array.isArray(result.suggestions));
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { describe, it, before, after } = require('node:test');
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
const brain = require('../brain.js');
|
|
9
|
+
|
|
10
|
+
let testDir;
|
|
11
|
+
let testDbPath;
|
|
12
|
+
|
|
13
|
+
function setupTestDb() {
|
|
14
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wall-e-slack-ingest-test-'));
|
|
15
|
+
testDbPath = path.join(testDir, 'test-brain.db');
|
|
16
|
+
brain.initDb(testDbPath);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function teardownTestDb() {
|
|
20
|
+
try { brain.closeDb(); } catch (_) {}
|
|
21
|
+
if (testDir && fs.existsSync(testDir)) {
|
|
22
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('Slack Ingest Module', () => {
|
|
27
|
+
before(() => setupTestDb());
|
|
28
|
+
after(() => teardownTestDb());
|
|
29
|
+
|
|
30
|
+
it('getSlackToken() returns a string or null', () => {
|
|
31
|
+
const slackIngest = require('../skills/slack-ingest');
|
|
32
|
+
const token = slackIngest.getSlackToken();
|
|
33
|
+
assert.ok(token === null || typeof token === 'string',
|
|
34
|
+
'getSlackToken should return null or a string');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('getProgress() returns object with expected fields', () => {
|
|
38
|
+
const slackIngest = require('../skills/slack-ingest');
|
|
39
|
+
const progress = slackIngest.getProgress();
|
|
40
|
+
assert.ok(typeof progress === 'object', 'getProgress should return an object');
|
|
41
|
+
assert.ok('phase' in progress, 'progress should have phase');
|
|
42
|
+
assert.ok('conversations_total' in progress, 'progress should have conversations_total');
|
|
43
|
+
assert.ok('conversations_processed' in progress, 'progress should have conversations_processed');
|
|
44
|
+
assert.ok('messages_ingested' in progress, 'progress should have messages_ingested');
|
|
45
|
+
assert.ok('started_at' in progress, 'progress should have started_at');
|
|
46
|
+
assert.ok('completed_at' in progress, 'progress should have completed_at');
|
|
47
|
+
assert.ok('percent' in progress, 'progress should have percent');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('getProgress() returns default state when no checkpoint exists', () => {
|
|
51
|
+
const slackIngest = require('../skills/slack-ingest');
|
|
52
|
+
const progress = slackIngest.getProgress();
|
|
53
|
+
assert.equal(progress.phase, 'conversations', 'default phase should be conversations');
|
|
54
|
+
assert.equal(progress.conversations_total, 0, 'default conversations_total should be 0');
|
|
55
|
+
assert.equal(progress.conversations_processed, 0, 'default conversations_processed should be 0');
|
|
56
|
+
assert.equal(progress.messages_ingested, 0, 'default messages_ingested should be 0');
|
|
57
|
+
assert.equal(progress.completed_at, null, 'default completed_at should be null');
|
|
58
|
+
assert.equal(progress.percent, 0, 'default percent should be 0');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('resetCheckpoint() clears state back to defaults', () => {
|
|
62
|
+
const slackIngest = require('../skills/slack-ingest');
|
|
63
|
+
|
|
64
|
+
// First save some custom state
|
|
65
|
+
brain.upsertCheckpoint('slack-ingest', {
|
|
66
|
+
last_memory_at: new Date().toISOString(),
|
|
67
|
+
metadata: JSON.stringify({
|
|
68
|
+
phase: 'history',
|
|
69
|
+
conversations: [{ id: 'C1', name: 'test', type: 'im' }],
|
|
70
|
+
current_conv_index: 1,
|
|
71
|
+
total_messages: 42,
|
|
72
|
+
total_conversations: 1,
|
|
73
|
+
started_at: '2024-01-01T00:00:00.000Z',
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Verify non-default state
|
|
78
|
+
let progress = slackIngest.getProgress();
|
|
79
|
+
assert.equal(progress.phase, 'history');
|
|
80
|
+
assert.equal(progress.messages_ingested, 42);
|
|
81
|
+
|
|
82
|
+
// Reset
|
|
83
|
+
slackIngest.resetCheckpoint();
|
|
84
|
+
|
|
85
|
+
// Verify reset
|
|
86
|
+
progress = slackIngest.getProgress();
|
|
87
|
+
assert.equal(progress.phase, 'conversations', 'phase should be reset to conversations');
|
|
88
|
+
assert.equal(progress.conversations_total, 0, 'conversations_total should be reset to 0');
|
|
89
|
+
assert.equal(progress.messages_ingested, 0, 'messages_ingested should be reset to 0');
|
|
90
|
+
assert.equal(progress.conversations_processed, 0, 'conversations_processed should be reset to 0');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('runIngestBatch() returns no_token error when no Slack token', async () => {
|
|
94
|
+
const slackIngest = require('../skills/slack-ingest');
|
|
95
|
+
// Unless the user happens to have a valid token, this should fail gracefully
|
|
96
|
+
const result = await slackIngest.runIngestBatch();
|
|
97
|
+
// Either no_token error or it actually has a token and starts working
|
|
98
|
+
assert.ok(typeof result === 'object', 'runIngestBatch should return an object');
|
|
99
|
+
assert.ok('done' in result, 'result should have done field');
|
|
100
|
+
assert.ok('messagesIngested' in result, 'result should have messagesIngested field');
|
|
101
|
+
assert.ok('conversationsProcessed' in result, 'result should have conversationsProcessed field');
|
|
102
|
+
});
|
|
103
|
+
});
|