@rozek/nanoclaw 1.2.17
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/.claude/settings.json +1 -0
- package/.claude/skills/add-compact/SKILL.md +135 -0
- package/.claude/skills/add-discord/SKILL.md +203 -0
- package/.claude/skills/add-gmail/SKILL.md +220 -0
- package/.claude/skills/add-image-vision/SKILL.md +94 -0
- package/.claude/skills/add-ollama-tool/SKILL.md +153 -0
- package/.claude/skills/add-parallel/SKILL.md +290 -0
- package/.claude/skills/add-pdf-reader/SKILL.md +104 -0
- package/.claude/skills/add-reactions/SKILL.md +117 -0
- package/.claude/skills/add-slack/SKILL.md +207 -0
- package/.claude/skills/add-telegram/SKILL.md +222 -0
- package/.claude/skills/add-telegram-swarm/SKILL.md +384 -0
- package/.claude/skills/add-voice-transcription/SKILL.md +148 -0
- package/.claude/skills/add-whatsapp/SKILL.md +372 -0
- package/.claude/skills/convert-to-apple-container/SKILL.md +175 -0
- package/.claude/skills/customize/SKILL.md +110 -0
- package/.claude/skills/debug/SKILL.md +349 -0
- package/.claude/skills/get-qodo-rules/SKILL.md +122 -0
- package/.claude/skills/get-qodo-rules/references/output-format.md +41 -0
- package/.claude/skills/get-qodo-rules/references/pagination.md +33 -0
- package/.claude/skills/get-qodo-rules/references/repository-scope.md +26 -0
- package/.claude/skills/qodo-pr-resolver/SKILL.md +326 -0
- package/.claude/skills/qodo-pr-resolver/resources/providers.md +329 -0
- package/.claude/skills/setup/SKILL.md +218 -0
- package/.claude/skills/update-nanoclaw/SKILL.md +235 -0
- package/.claude/skills/update-skills/SKILL.md +130 -0
- package/.claude/skills/use-local-whisper/SKILL.md +152 -0
- package/.claude/skills/x-integration/SKILL.md +417 -0
- package/.claude/skills/x-integration/agent.ts +243 -0
- package/.claude/skills/x-integration/host.ts +159 -0
- package/.claude/skills/x-integration/lib/browser.ts +148 -0
- package/.claude/skills/x-integration/lib/config.ts +62 -0
- package/.claude/skills/x-integration/scripts/like.ts +56 -0
- package/.claude/skills/x-integration/scripts/post.ts +66 -0
- package/.claude/skills/x-integration/scripts/quote.ts +80 -0
- package/.claude/skills/x-integration/scripts/reply.ts +74 -0
- package/.claude/skills/x-integration/scripts/retweet.ts +62 -0
- package/.claude/skills/x-integration/scripts/setup.ts +87 -0
- package/.env.example +1 -0
- package/.github/CODEOWNERS +10 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +14 -0
- package/.github/workflows/bump-version.yml +32 -0
- package/.github/workflows/ci.yml +25 -0
- package/.github/workflows/merge-forward-skills.yml +160 -0
- package/.github/workflows/update-tokens.yml +42 -0
- package/.husky/pre-commit +1 -0
- package/.mcp.json +3 -0
- package/.nvmrc +1 -0
- package/.prettierrc +3 -0
- package/CHANGELOG.md +8 -0
- package/CLAUDE.md +64 -0
- package/CONTRIBUTING.md +23 -0
- package/CONTRIBUTORS.md +15 -0
- package/LICENSE +21 -0
- package/NanoClaw_with_Web-Support.md +290 -0
- package/README.md +261 -0
- package/README_zh.md +200 -0
- package/assets/nanoclaw-favicon.png +0 -0
- package/assets/nanoclaw-icon.png +0 -0
- package/assets/nanoclaw-logo-dark.png +0 -0
- package/assets/nanoclaw-logo.png +0 -0
- package/assets/nanoclaw-profile.jpeg +0 -0
- package/assets/nanoclaw-sales.png +0 -0
- package/assets/social-preview.jpg +0 -0
- package/config-examples/mount-allowlist.json +25 -0
- package/container/Dockerfile +70 -0
- package/container/agent-runner/package-lock.json +1524 -0
- package/container/agent-runner/package.json +21 -0
- package/container/agent-runner/src/index.ts +558 -0
- package/container/agent-runner/src/ipc-mcp-stdio.ts +338 -0
- package/container/agent-runner/tsconfig.json +15 -0
- package/container/build.sh +23 -0
- package/container/skills/agent-browser/SKILL.md +159 -0
- package/container/skills/capabilities/SKILL.md +100 -0
- package/container/skills/status/SKILL.md +104 -0
- package/dist/channels/index.d.ts +2 -0
- package/dist/channels/index.d.ts.map +1 -0
- package/dist/channels/index.js +9 -0
- package/dist/channels/index.js.map +1 -0
- package/dist/channels/registry.d.ts +13 -0
- package/dist/channels/registry.d.ts.map +1 -0
- package/dist/channels/registry.js +11 -0
- package/dist/channels/registry.js.map +1 -0
- package/dist/channels/registry.test.d.ts +2 -0
- package/dist/channels/registry.test.d.ts.map +1 -0
- package/dist/channels/registry.test.js +32 -0
- package/dist/channels/registry.test.js.map +1 -0
- package/dist/channels/web.d.ts +2 -0
- package/dist/channels/web.d.ts.map +1 -0
- package/dist/channels/web.js +1738 -0
- package/dist/channels/web.js.map +1 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +182 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +36 -0
- package/dist/config.js.map +1 -0
- package/dist/container-runner.d.ts +44 -0
- package/dist/container-runner.d.ts.map +1 -0
- package/dist/container-runner.js +467 -0
- package/dist/container-runner.js.map +1 -0
- package/dist/container-runner.test.d.ts +2 -0
- package/dist/container-runner.test.d.ts.map +1 -0
- package/dist/container-runner.test.js +150 -0
- package/dist/container-runner.test.js.map +1 -0
- package/dist/container-runtime.d.ts +22 -0
- package/dist/container-runtime.d.ts.map +1 -0
- package/dist/container-runtime.js +96 -0
- package/dist/container-runtime.js.map +1 -0
- package/dist/container-runtime.test.d.ts +2 -0
- package/dist/container-runtime.test.d.ts.map +1 -0
- package/dist/container-runtime.test.js +93 -0
- package/dist/container-runtime.test.js.map +1 -0
- package/dist/credential-proxy.d.ts +21 -0
- package/dist/credential-proxy.d.ts.map +1 -0
- package/dist/credential-proxy.js +95 -0
- package/dist/credential-proxy.js.map +1 -0
- package/dist/credential-proxy.test.d.ts +2 -0
- package/dist/credential-proxy.test.d.ts.map +1 -0
- package/dist/credential-proxy.test.js +134 -0
- package/dist/credential-proxy.test.js.map +1 -0
- package/dist/db.d.ts +115 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +549 -0
- package/dist/db.js.map +1 -0
- package/dist/db.test.d.ts +2 -0
- package/dist/db.test.d.ts.map +1 -0
- package/dist/db.test.js +360 -0
- package/dist/db.test.js.map +1 -0
- package/dist/env.d.ts +8 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +42 -0
- package/dist/env.js.map +1 -0
- package/dist/formatting.test.d.ts +2 -0
- package/dist/formatting.test.d.ts.map +1 -0
- package/dist/formatting.test.js +183 -0
- package/dist/formatting.test.js.map +1 -0
- package/dist/group-folder.d.ts +5 -0
- package/dist/group-folder.d.ts.map +1 -0
- package/dist/group-folder.js +44 -0
- package/dist/group-folder.js.map +1 -0
- package/dist/group-folder.test.d.ts +2 -0
- package/dist/group-folder.test.d.ts.map +1 -0
- package/dist/group-folder.test.js +29 -0
- package/dist/group-folder.test.js.map +1 -0
- package/dist/group-queue.d.ts +34 -0
- package/dist/group-queue.d.ts.map +1 -0
- package/dist/group-queue.js +263 -0
- package/dist/group-queue.js.map +1 -0
- package/dist/group-queue.test.d.ts +2 -0
- package/dist/group-queue.test.d.ts.map +1 -0
- package/dist/group-queue.test.js +341 -0
- package/dist/group-queue.test.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +518 -0
- package/dist/index.js.map +1 -0
- package/dist/ipc-auth.test.d.ts +2 -0
- package/dist/ipc-auth.test.d.ts.map +1 -0
- package/dist/ipc-auth.test.js +434 -0
- package/dist/ipc-auth.test.js.map +1 -0
- package/dist/ipc.d.ts +32 -0
- package/dist/ipc.d.ts.map +1 -0
- package/dist/ipc.js +311 -0
- package/dist/ipc.js.map +1 -0
- package/dist/logger.d.ts +3 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +14 -0
- package/dist/logger.js.map +1 -0
- package/dist/mount-security.d.ts +34 -0
- package/dist/mount-security.d.ts.map +1 -0
- package/dist/mount-security.js +325 -0
- package/dist/mount-security.js.map +1 -0
- package/dist/remote-control.d.ts +32 -0
- package/dist/remote-control.d.ts.map +1 -0
- package/dist/remote-control.js +185 -0
- package/dist/remote-control.js.map +1 -0
- package/dist/remote-control.test.d.ts +2 -0
- package/dist/remote-control.test.d.ts.map +1 -0
- package/dist/remote-control.test.js +321 -0
- package/dist/remote-control.test.js.map +1 -0
- package/dist/router.d.ts +8 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +37 -0
- package/dist/router.js.map +1 -0
- package/dist/routing.test.d.ts +2 -0
- package/dist/routing.test.d.ts.map +1 -0
- package/dist/routing.test.js +81 -0
- package/dist/routing.test.js.map +1 -0
- package/dist/sender-allowlist.d.ts +14 -0
- package/dist/sender-allowlist.d.ts.map +1 -0
- package/dist/sender-allowlist.js +79 -0
- package/dist/sender-allowlist.js.map +1 -0
- package/dist/sender-allowlist.test.d.ts +2 -0
- package/dist/sender-allowlist.test.d.ts.map +1 -0
- package/dist/sender-allowlist.test.js +186 -0
- package/dist/sender-allowlist.test.js.map +1 -0
- package/dist/session-commands.d.ts +47 -0
- package/dist/session-commands.d.ts.map +1 -0
- package/dist/session-commands.js +102 -0
- package/dist/session-commands.js.map +1 -0
- package/dist/session-commands.test.d.ts +2 -0
- package/dist/session-commands.test.d.ts.map +1 -0
- package/dist/session-commands.test.js +190 -0
- package/dist/session-commands.test.js.map +1 -0
- package/dist/task-scheduler.d.ts +22 -0
- package/dist/task-scheduler.d.ts.map +1 -0
- package/dist/task-scheduler.js +210 -0
- package/dist/task-scheduler.js.map +1 -0
- package/dist/task-scheduler.test.d.ts +2 -0
- package/dist/task-scheduler.test.d.ts.map +1 -0
- package/dist/task-scheduler.test.js +107 -0
- package/dist/task-scheduler.test.js.map +1 -0
- package/dist/timezone.d.ts +6 -0
- package/dist/timezone.d.ts.map +1 -0
- package/dist/timezone.js +17 -0
- package/dist/timezone.js.map +1 -0
- package/dist/timezone.test.d.ts +2 -0
- package/dist/timezone.test.d.ts.map +1 -0
- package/dist/timezone.test.js +23 -0
- package/dist/timezone.test.js.map +1 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/docs/APPLE-CONTAINER-NETWORKING.md +90 -0
- package/docs/DEBUG_CHECKLIST.md +143 -0
- package/docs/REQUIREMENTS.md +196 -0
- package/docs/SDK_DEEP_DIVE.md +643 -0
- package/docs/SECURITY.md +122 -0
- package/docs/SPEC.md +785 -0
- package/docs/docker-sandboxes.md +359 -0
- package/docs/nanoclaw-architecture-final.md +1063 -0
- package/docs/nanorepo-architecture.md +168 -0
- package/docs/skills-as-branches.md +662 -0
- package/groups/global/CLAUDE.md +58 -0
- package/groups/main/CLAUDE.md +246 -0
- package/launchd/com.nanoclaw.plist +32 -0
- package/package.json +45 -0
- package/repo-tokens/README.md +113 -0
- package/repo-tokens/action.yml +186 -0
- package/repo-tokens/badge.svg +23 -0
- package/repo-tokens/examples/green.svg +14 -0
- package/repo-tokens/examples/red.svg +14 -0
- package/repo-tokens/examples/yellow-green.svg +14 -0
- package/repo-tokens/examples/yellow.svg +14 -0
- package/scripts/run-migrations.ts +105 -0
- package/setup/container.ts +144 -0
- package/setup/environment.test.ts +121 -0
- package/setup/environment.ts +94 -0
- package/setup/groups.ts +229 -0
- package/setup/index.ts +58 -0
- package/setup/mounts.ts +115 -0
- package/setup/platform.test.ts +120 -0
- package/setup/platform.ts +132 -0
- package/setup/register.test.ts +257 -0
- package/setup/register.ts +177 -0
- package/setup/service.test.ts +187 -0
- package/setup/service.ts +362 -0
- package/setup/status.ts +16 -0
- package/setup/verify.ts +192 -0
- package/setup.sh +161 -0
- package/src/channels/index.ts +12 -0
- package/src/channels/registry.test.ts +42 -0
- package/src/channels/registry.ts +32 -0
- package/src/channels/web.ts +1856 -0
- package/src/cli.ts +209 -0
- package/src/config.ts +73 -0
- package/src/container-runner.test.ts +210 -0
- package/src/container-runner.ts +707 -0
- package/src/container-runtime.test.ts +149 -0
- package/src/container-runtime.ts +127 -0
- package/src/credential-proxy.test.ts +192 -0
- package/src/credential-proxy.ts +125 -0
- package/src/db.test.ts +484 -0
- package/src/db.ts +803 -0
- package/src/env.ts +42 -0
- package/src/formatting.test.ts +256 -0
- package/src/group-folder.test.ts +43 -0
- package/src/group-folder.ts +44 -0
- package/src/group-queue.test.ts +484 -0
- package/src/group-queue.ts +365 -0
- package/src/index.ts +731 -0
- package/src/ipc-auth.test.ts +679 -0
- package/src/ipc.ts +461 -0
- package/src/logger.ts +16 -0
- package/src/mount-security.ts +419 -0
- package/src/remote-control.test.ts +397 -0
- package/src/remote-control.ts +224 -0
- package/src/router.ts +52 -0
- package/src/routing.test.ts +170 -0
- package/src/sender-allowlist.test.ts +216 -0
- package/src/sender-allowlist.ts +128 -0
- package/src/session-commands.test.ts +247 -0
- package/src/session-commands.ts +163 -0
- package/src/task-scheduler.test.ts +129 -0
- package/src/task-scheduler.ts +295 -0
- package/src/timezone.test.ts +29 -0
- package/src/timezone.ts +16 -0
- package/src/types.ts +107 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +7 -0
- package/vitest.skills.config.ts +7 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import Database from 'better-sqlite3';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tests for the register step.
|
|
7
|
+
*
|
|
8
|
+
* Verifies: parameterized SQL (no injection), file templating,
|
|
9
|
+
* apostrophe in names, .env updates.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
function createTestDb(): Database.Database {
|
|
13
|
+
const db = new Database(':memory:');
|
|
14
|
+
db.exec(`CREATE TABLE IF NOT EXISTS registered_groups (
|
|
15
|
+
jid TEXT PRIMARY KEY,
|
|
16
|
+
name TEXT NOT NULL,
|
|
17
|
+
folder TEXT NOT NULL UNIQUE,
|
|
18
|
+
trigger_pattern TEXT NOT NULL,
|
|
19
|
+
added_at TEXT NOT NULL,
|
|
20
|
+
container_config TEXT,
|
|
21
|
+
requires_trigger INTEGER DEFAULT 1,
|
|
22
|
+
is_main INTEGER DEFAULT 0
|
|
23
|
+
)`);
|
|
24
|
+
return db;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('parameterized SQL registration', () => {
|
|
28
|
+
let db: Database.Database;
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
db = createTestDb();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('registers a group with parameterized query', () => {
|
|
35
|
+
db.prepare(
|
|
36
|
+
`INSERT OR REPLACE INTO registered_groups
|
|
37
|
+
(jid, name, folder, trigger_pattern, added_at, container_config, requires_trigger)
|
|
38
|
+
VALUES (?, ?, ?, ?, ?, NULL, ?)`,
|
|
39
|
+
).run(
|
|
40
|
+
'123@g.us',
|
|
41
|
+
'Test Group',
|
|
42
|
+
'test-group',
|
|
43
|
+
'@Andy',
|
|
44
|
+
'2024-01-01T00:00:00.000Z',
|
|
45
|
+
1,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const row = db
|
|
49
|
+
.prepare('SELECT * FROM registered_groups WHERE jid = ?')
|
|
50
|
+
.get('123@g.us') as {
|
|
51
|
+
jid: string;
|
|
52
|
+
name: string;
|
|
53
|
+
folder: string;
|
|
54
|
+
trigger_pattern: string;
|
|
55
|
+
requires_trigger: number;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
expect(row.jid).toBe('123@g.us');
|
|
59
|
+
expect(row.name).toBe('Test Group');
|
|
60
|
+
expect(row.folder).toBe('test-group');
|
|
61
|
+
expect(row.trigger_pattern).toBe('@Andy');
|
|
62
|
+
expect(row.requires_trigger).toBe(1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('handles apostrophes in group names safely', () => {
|
|
66
|
+
const name = "O'Brien's Group";
|
|
67
|
+
|
|
68
|
+
db.prepare(
|
|
69
|
+
`INSERT OR REPLACE INTO registered_groups
|
|
70
|
+
(jid, name, folder, trigger_pattern, added_at, container_config, requires_trigger)
|
|
71
|
+
VALUES (?, ?, ?, ?, ?, NULL, ?)`,
|
|
72
|
+
).run(
|
|
73
|
+
'456@g.us',
|
|
74
|
+
name,
|
|
75
|
+
'obriens-group',
|
|
76
|
+
'@Andy',
|
|
77
|
+
'2024-01-01T00:00:00.000Z',
|
|
78
|
+
0,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const row = db
|
|
82
|
+
.prepare('SELECT name FROM registered_groups WHERE jid = ?')
|
|
83
|
+
.get('456@g.us') as {
|
|
84
|
+
name: string;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
expect(row.name).toBe(name);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('prevents SQL injection in JID field', () => {
|
|
91
|
+
const maliciousJid = "'; DROP TABLE registered_groups; --";
|
|
92
|
+
|
|
93
|
+
db.prepare(
|
|
94
|
+
`INSERT OR REPLACE INTO registered_groups
|
|
95
|
+
(jid, name, folder, trigger_pattern, added_at, container_config, requires_trigger)
|
|
96
|
+
VALUES (?, ?, ?, ?, ?, NULL, ?)`,
|
|
97
|
+
).run(maliciousJid, 'Evil', 'evil', '@Andy', '2024-01-01T00:00:00.000Z', 1);
|
|
98
|
+
|
|
99
|
+
// Table should still exist and have the row
|
|
100
|
+
const count = db
|
|
101
|
+
.prepare('SELECT COUNT(*) as count FROM registered_groups')
|
|
102
|
+
.get() as {
|
|
103
|
+
count: number;
|
|
104
|
+
};
|
|
105
|
+
expect(count.count).toBe(1);
|
|
106
|
+
|
|
107
|
+
const row = db.prepare('SELECT jid FROM registered_groups').get() as {
|
|
108
|
+
jid: string;
|
|
109
|
+
};
|
|
110
|
+
expect(row.jid).toBe(maliciousJid);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('handles requiresTrigger=false', () => {
|
|
114
|
+
db.prepare(
|
|
115
|
+
`INSERT OR REPLACE INTO registered_groups
|
|
116
|
+
(jid, name, folder, trigger_pattern, added_at, container_config, requires_trigger)
|
|
117
|
+
VALUES (?, ?, ?, ?, ?, NULL, ?)`,
|
|
118
|
+
).run(
|
|
119
|
+
'789@s.whatsapp.net',
|
|
120
|
+
'Personal',
|
|
121
|
+
'main',
|
|
122
|
+
'@Andy',
|
|
123
|
+
'2024-01-01T00:00:00.000Z',
|
|
124
|
+
0,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const row = db
|
|
128
|
+
.prepare('SELECT requires_trigger FROM registered_groups WHERE jid = ?')
|
|
129
|
+
.get('789@s.whatsapp.net') as { requires_trigger: number };
|
|
130
|
+
|
|
131
|
+
expect(row.requires_trigger).toBe(0);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('stores is_main flag', () => {
|
|
135
|
+
db.prepare(
|
|
136
|
+
`INSERT OR REPLACE INTO registered_groups
|
|
137
|
+
(jid, name, folder, trigger_pattern, added_at, container_config, requires_trigger, is_main)
|
|
138
|
+
VALUES (?, ?, ?, ?, ?, NULL, ?, ?)`,
|
|
139
|
+
).run(
|
|
140
|
+
'789@s.whatsapp.net',
|
|
141
|
+
'Personal',
|
|
142
|
+
'whatsapp_main',
|
|
143
|
+
'@Andy',
|
|
144
|
+
'2024-01-01T00:00:00.000Z',
|
|
145
|
+
0,
|
|
146
|
+
1,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const row = db
|
|
150
|
+
.prepare('SELECT is_main FROM registered_groups WHERE jid = ?')
|
|
151
|
+
.get('789@s.whatsapp.net') as { is_main: number };
|
|
152
|
+
|
|
153
|
+
expect(row.is_main).toBe(1);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('defaults is_main to 0', () => {
|
|
157
|
+
db.prepare(
|
|
158
|
+
`INSERT OR REPLACE INTO registered_groups
|
|
159
|
+
(jid, name, folder, trigger_pattern, added_at, container_config, requires_trigger)
|
|
160
|
+
VALUES (?, ?, ?, ?, ?, NULL, ?)`,
|
|
161
|
+
).run(
|
|
162
|
+
'123@g.us',
|
|
163
|
+
'Some Group',
|
|
164
|
+
'whatsapp_some-group',
|
|
165
|
+
'@Andy',
|
|
166
|
+
'2024-01-01T00:00:00.000Z',
|
|
167
|
+
1,
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const row = db
|
|
171
|
+
.prepare('SELECT is_main FROM registered_groups WHERE jid = ?')
|
|
172
|
+
.get('123@g.us') as { is_main: number };
|
|
173
|
+
|
|
174
|
+
expect(row.is_main).toBe(0);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('upserts on conflict', () => {
|
|
178
|
+
const stmt = db.prepare(
|
|
179
|
+
`INSERT OR REPLACE INTO registered_groups
|
|
180
|
+
(jid, name, folder, trigger_pattern, added_at, container_config, requires_trigger)
|
|
181
|
+
VALUES (?, ?, ?, ?, ?, NULL, ?)`,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
stmt.run(
|
|
185
|
+
'123@g.us',
|
|
186
|
+
'Original',
|
|
187
|
+
'main',
|
|
188
|
+
'@Andy',
|
|
189
|
+
'2024-01-01T00:00:00.000Z',
|
|
190
|
+
1,
|
|
191
|
+
);
|
|
192
|
+
stmt.run(
|
|
193
|
+
'123@g.us',
|
|
194
|
+
'Updated',
|
|
195
|
+
'main',
|
|
196
|
+
'@Bot',
|
|
197
|
+
'2024-02-01T00:00:00.000Z',
|
|
198
|
+
0,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const rows = db.prepare('SELECT * FROM registered_groups').all();
|
|
202
|
+
expect(rows).toHaveLength(1);
|
|
203
|
+
|
|
204
|
+
const row = rows[0] as {
|
|
205
|
+
name: string;
|
|
206
|
+
trigger_pattern: string;
|
|
207
|
+
requires_trigger: number;
|
|
208
|
+
};
|
|
209
|
+
expect(row.name).toBe('Updated');
|
|
210
|
+
expect(row.trigger_pattern).toBe('@Bot');
|
|
211
|
+
expect(row.requires_trigger).toBe(0);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('file templating', () => {
|
|
216
|
+
it('replaces assistant name in CLAUDE.md content', () => {
|
|
217
|
+
let content = '# Andy\n\nYou are Andy, a personal assistant.';
|
|
218
|
+
|
|
219
|
+
content = content.replace(/^# Andy$/m, '# Nova');
|
|
220
|
+
content = content.replace(/You are Andy/g, 'You are Nova');
|
|
221
|
+
|
|
222
|
+
expect(content).toBe('# Nova\n\nYou are Nova, a personal assistant.');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('handles names with special regex characters', () => {
|
|
226
|
+
let content = '# Andy\n\nYou are Andy.';
|
|
227
|
+
|
|
228
|
+
const newName = 'C.L.A.U.D.E';
|
|
229
|
+
content = content.replace(/^# Andy$/m, `# ${newName}`);
|
|
230
|
+
content = content.replace(/You are Andy/g, `You are ${newName}`);
|
|
231
|
+
|
|
232
|
+
expect(content).toContain('# C.L.A.U.D.E');
|
|
233
|
+
expect(content).toContain('You are C.L.A.U.D.E.');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('updates .env ASSISTANT_NAME line', () => {
|
|
237
|
+
let envContent = 'SOME_KEY=value\nASSISTANT_NAME="Andy"\nOTHER=test';
|
|
238
|
+
|
|
239
|
+
envContent = envContent.replace(
|
|
240
|
+
/^ASSISTANT_NAME=.*$/m,
|
|
241
|
+
'ASSISTANT_NAME="Nova"',
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
expect(envContent).toContain('ASSISTANT_NAME="Nova"');
|
|
245
|
+
expect(envContent).toContain('SOME_KEY=value');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('appends ASSISTANT_NAME to .env if not present', () => {
|
|
249
|
+
let envContent = 'SOME_KEY=value\n';
|
|
250
|
+
|
|
251
|
+
if (!envContent.includes('ASSISTANT_NAME=')) {
|
|
252
|
+
envContent += '\nASSISTANT_NAME="Nova"';
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
expect(envContent).toContain('ASSISTANT_NAME="Nova"');
|
|
256
|
+
});
|
|
257
|
+
});
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step: register — Write channel registration config, create group folders.
|
|
3
|
+
*
|
|
4
|
+
* Accepts --channel to specify the messaging platform (whatsapp, telegram, slack, discord).
|
|
5
|
+
* Uses parameterized SQL queries to prevent injection.
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
|
|
10
|
+
import { STORE_DIR } from '../src/config.ts';
|
|
11
|
+
import { initDatabase, setRegisteredGroup } from '../src/db.ts';
|
|
12
|
+
import { isValidGroupFolder } from '../src/group-folder.ts';
|
|
13
|
+
import { logger } from '../src/logger.ts';
|
|
14
|
+
import { emitStatus } from './status.ts';
|
|
15
|
+
|
|
16
|
+
interface RegisterArgs {
|
|
17
|
+
jid: string;
|
|
18
|
+
name: string;
|
|
19
|
+
trigger: string;
|
|
20
|
+
folder: string;
|
|
21
|
+
channel: string;
|
|
22
|
+
requiresTrigger: boolean;
|
|
23
|
+
isMain: boolean;
|
|
24
|
+
assistantName: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseArgs(args: string[]): RegisterArgs {
|
|
28
|
+
const result: RegisterArgs = {
|
|
29
|
+
jid: '',
|
|
30
|
+
name: '',
|
|
31
|
+
trigger: '',
|
|
32
|
+
folder: '',
|
|
33
|
+
channel: 'whatsapp', // backward-compat: pre-refactor installs omit --channel
|
|
34
|
+
requiresTrigger: true,
|
|
35
|
+
isMain: false,
|
|
36
|
+
assistantName: 'Andy',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
for (let i = 0; i < args.length; i++) {
|
|
40
|
+
switch (args[i]) {
|
|
41
|
+
case '--jid':
|
|
42
|
+
result.jid = args[++i] || '';
|
|
43
|
+
break;
|
|
44
|
+
case '--name':
|
|
45
|
+
result.name = args[++i] || '';
|
|
46
|
+
break;
|
|
47
|
+
case '--trigger':
|
|
48
|
+
result.trigger = args[++i] || '';
|
|
49
|
+
break;
|
|
50
|
+
case '--folder':
|
|
51
|
+
result.folder = args[++i] || '';
|
|
52
|
+
break;
|
|
53
|
+
case '--channel':
|
|
54
|
+
result.channel = (args[++i] || '').toLowerCase();
|
|
55
|
+
break;
|
|
56
|
+
case '--no-trigger-required':
|
|
57
|
+
result.requiresTrigger = false;
|
|
58
|
+
break;
|
|
59
|
+
case '--is-main':
|
|
60
|
+
result.isMain = true;
|
|
61
|
+
break;
|
|
62
|
+
case '--assistant-name':
|
|
63
|
+
result.assistantName = args[++i] || 'Andy';
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function run(args: string[]): Promise<void> {
|
|
72
|
+
const projectRoot = process.cwd();
|
|
73
|
+
const parsed = parseArgs(args);
|
|
74
|
+
|
|
75
|
+
if (!parsed.jid || !parsed.name || !parsed.trigger || !parsed.folder) {
|
|
76
|
+
emitStatus('REGISTER_CHANNEL', {
|
|
77
|
+
STATUS: 'failed',
|
|
78
|
+
ERROR: 'missing_required_args',
|
|
79
|
+
LOG: 'logs/setup.log',
|
|
80
|
+
});
|
|
81
|
+
process.exit(4);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!isValidGroupFolder(parsed.folder)) {
|
|
85
|
+
emitStatus('REGISTER_CHANNEL', {
|
|
86
|
+
STATUS: 'failed',
|
|
87
|
+
ERROR: 'invalid_folder',
|
|
88
|
+
LOG: 'logs/setup.log',
|
|
89
|
+
});
|
|
90
|
+
process.exit(4);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
logger.info(parsed, 'Registering channel');
|
|
94
|
+
|
|
95
|
+
// Ensure data and store directories exist (store/ may not exist on
|
|
96
|
+
// fresh installs that skip WhatsApp auth, which normally creates it)
|
|
97
|
+
fs.mkdirSync(path.join(projectRoot, 'data'), { recursive: true });
|
|
98
|
+
fs.mkdirSync(STORE_DIR, { recursive: true });
|
|
99
|
+
|
|
100
|
+
// Initialize database (creates schema + runs migrations)
|
|
101
|
+
initDatabase();
|
|
102
|
+
|
|
103
|
+
setRegisteredGroup(parsed.jid, {
|
|
104
|
+
name: parsed.name,
|
|
105
|
+
folder: parsed.folder,
|
|
106
|
+
trigger: parsed.trigger,
|
|
107
|
+
added_at: new Date().toISOString(),
|
|
108
|
+
requiresTrigger: parsed.requiresTrigger,
|
|
109
|
+
isMain: parsed.isMain,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
logger.info('Wrote registration to SQLite');
|
|
113
|
+
|
|
114
|
+
// Create group folders
|
|
115
|
+
fs.mkdirSync(path.join(projectRoot, 'groups', parsed.folder, 'logs'), {
|
|
116
|
+
recursive: true,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Update assistant name in CLAUDE.md files if different from default
|
|
120
|
+
let nameUpdated = false;
|
|
121
|
+
if (parsed.assistantName !== 'Andy') {
|
|
122
|
+
logger.info(
|
|
123
|
+
{ from: 'Andy', to: parsed.assistantName },
|
|
124
|
+
'Updating assistant name',
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const mdFiles = [
|
|
128
|
+
path.join(projectRoot, 'groups', 'global', 'CLAUDE.md'),
|
|
129
|
+
path.join(projectRoot, 'groups', parsed.folder, 'CLAUDE.md'),
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
for (const mdFile of mdFiles) {
|
|
133
|
+
if (fs.existsSync(mdFile)) {
|
|
134
|
+
let content = fs.readFileSync(mdFile, 'utf-8');
|
|
135
|
+
content = content.replace(/^# Andy$/m, `# ${parsed.assistantName}`);
|
|
136
|
+
content = content.replace(
|
|
137
|
+
/You are Andy/g,
|
|
138
|
+
`You are ${parsed.assistantName}`,
|
|
139
|
+
);
|
|
140
|
+
fs.writeFileSync(mdFile, content);
|
|
141
|
+
logger.info({ file: mdFile }, 'Updated CLAUDE.md');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Update .env
|
|
146
|
+
const envFile = path.join(projectRoot, '.env');
|
|
147
|
+
if (fs.existsSync(envFile)) {
|
|
148
|
+
let envContent = fs.readFileSync(envFile, 'utf-8');
|
|
149
|
+
if (envContent.includes('ASSISTANT_NAME=')) {
|
|
150
|
+
envContent = envContent.replace(
|
|
151
|
+
/^ASSISTANT_NAME=.*$/m,
|
|
152
|
+
`ASSISTANT_NAME="${parsed.assistantName}"`,
|
|
153
|
+
);
|
|
154
|
+
} else {
|
|
155
|
+
envContent += `\nASSISTANT_NAME="${parsed.assistantName}"`;
|
|
156
|
+
}
|
|
157
|
+
fs.writeFileSync(envFile, envContent);
|
|
158
|
+
} else {
|
|
159
|
+
fs.writeFileSync(envFile, `ASSISTANT_NAME="${parsed.assistantName}"\n`);
|
|
160
|
+
}
|
|
161
|
+
logger.info('Set ASSISTANT_NAME in .env');
|
|
162
|
+
nameUpdated = true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
emitStatus('REGISTER_CHANNEL', {
|
|
166
|
+
JID: parsed.jid,
|
|
167
|
+
NAME: parsed.name,
|
|
168
|
+
FOLDER: parsed.folder,
|
|
169
|
+
CHANNEL: parsed.channel,
|
|
170
|
+
TRIGGER: parsed.trigger,
|
|
171
|
+
REQUIRES_TRIGGER: parsed.requiresTrigger,
|
|
172
|
+
ASSISTANT_NAME: parsed.assistantName,
|
|
173
|
+
NAME_UPDATED: nameUpdated,
|
|
174
|
+
STATUS: 'success',
|
|
175
|
+
LOG: 'logs/setup.log',
|
|
176
|
+
});
|
|
177
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tests for service configuration generation.
|
|
6
|
+
*
|
|
7
|
+
* These tests verify the generated content of plist/systemd/nohup configs
|
|
8
|
+
* without actually loading services.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Helper: generate a plist string the same way service.ts does
|
|
12
|
+
function generatePlist(
|
|
13
|
+
nodePath: string,
|
|
14
|
+
projectRoot: string,
|
|
15
|
+
homeDir: string,
|
|
16
|
+
): string {
|
|
17
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
18
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
19
|
+
<plist version="1.0">
|
|
20
|
+
<dict>
|
|
21
|
+
<key>Label</key>
|
|
22
|
+
<string>com.nanoclaw</string>
|
|
23
|
+
<key>ProgramArguments</key>
|
|
24
|
+
<array>
|
|
25
|
+
<string>${nodePath}</string>
|
|
26
|
+
<string>${projectRoot}/dist/index.js</string>
|
|
27
|
+
</array>
|
|
28
|
+
<key>WorkingDirectory</key>
|
|
29
|
+
<string>${projectRoot}</string>
|
|
30
|
+
<key>RunAtLoad</key>
|
|
31
|
+
<true/>
|
|
32
|
+
<key>KeepAlive</key>
|
|
33
|
+
<true/>
|
|
34
|
+
<key>EnvironmentVariables</key>
|
|
35
|
+
<dict>
|
|
36
|
+
<key>PATH</key>
|
|
37
|
+
<string>/usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin</string>
|
|
38
|
+
<key>HOME</key>
|
|
39
|
+
<string>${homeDir}</string>
|
|
40
|
+
</dict>
|
|
41
|
+
<key>StandardOutPath</key>
|
|
42
|
+
<string>${projectRoot}/logs/nanoclaw.log</string>
|
|
43
|
+
<key>StandardErrorPath</key>
|
|
44
|
+
<string>${projectRoot}/logs/nanoclaw.error.log</string>
|
|
45
|
+
</dict>
|
|
46
|
+
</plist>`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function generateSystemdUnit(
|
|
50
|
+
nodePath: string,
|
|
51
|
+
projectRoot: string,
|
|
52
|
+
homeDir: string,
|
|
53
|
+
isSystem: boolean,
|
|
54
|
+
): string {
|
|
55
|
+
return `[Unit]
|
|
56
|
+
Description=NanoClaw Personal Assistant
|
|
57
|
+
After=network.target
|
|
58
|
+
|
|
59
|
+
[Service]
|
|
60
|
+
Type=simple
|
|
61
|
+
ExecStart=${nodePath} ${projectRoot}/dist/index.js
|
|
62
|
+
WorkingDirectory=${projectRoot}
|
|
63
|
+
Restart=always
|
|
64
|
+
RestartSec=5
|
|
65
|
+
KillMode=process
|
|
66
|
+
Environment=HOME=${homeDir}
|
|
67
|
+
Environment=PATH=/usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin
|
|
68
|
+
StandardOutput=append:${projectRoot}/logs/nanoclaw.log
|
|
69
|
+
StandardError=append:${projectRoot}/logs/nanoclaw.error.log
|
|
70
|
+
|
|
71
|
+
[Install]
|
|
72
|
+
WantedBy=${isSystem ? 'multi-user.target' : 'default.target'}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe('plist generation', () => {
|
|
76
|
+
it('contains the correct label', () => {
|
|
77
|
+
const plist = generatePlist(
|
|
78
|
+
'/usr/local/bin/node',
|
|
79
|
+
'/home/user/nanoclaw',
|
|
80
|
+
'/home/user',
|
|
81
|
+
);
|
|
82
|
+
expect(plist).toContain('<string>com.nanoclaw</string>');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('uses the correct node path', () => {
|
|
86
|
+
const plist = generatePlist(
|
|
87
|
+
'/opt/node/bin/node',
|
|
88
|
+
'/home/user/nanoclaw',
|
|
89
|
+
'/home/user',
|
|
90
|
+
);
|
|
91
|
+
expect(plist).toContain('<string>/opt/node/bin/node</string>');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('points to dist/index.js', () => {
|
|
95
|
+
const plist = generatePlist(
|
|
96
|
+
'/usr/local/bin/node',
|
|
97
|
+
'/home/user/nanoclaw',
|
|
98
|
+
'/home/user',
|
|
99
|
+
);
|
|
100
|
+
expect(plist).toContain('/home/user/nanoclaw/dist/index.js');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('sets log paths', () => {
|
|
104
|
+
const plist = generatePlist(
|
|
105
|
+
'/usr/local/bin/node',
|
|
106
|
+
'/home/user/nanoclaw',
|
|
107
|
+
'/home/user',
|
|
108
|
+
);
|
|
109
|
+
expect(plist).toContain('nanoclaw.log');
|
|
110
|
+
expect(plist).toContain('nanoclaw.error.log');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('systemd unit generation', () => {
|
|
115
|
+
it('user unit uses default.target', () => {
|
|
116
|
+
const unit = generateSystemdUnit(
|
|
117
|
+
'/usr/bin/node',
|
|
118
|
+
'/home/user/nanoclaw',
|
|
119
|
+
'/home/user',
|
|
120
|
+
false,
|
|
121
|
+
);
|
|
122
|
+
expect(unit).toContain('WantedBy=default.target');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('system unit uses multi-user.target', () => {
|
|
126
|
+
const unit = generateSystemdUnit(
|
|
127
|
+
'/usr/bin/node',
|
|
128
|
+
'/home/user/nanoclaw',
|
|
129
|
+
'/home/user',
|
|
130
|
+
true,
|
|
131
|
+
);
|
|
132
|
+
expect(unit).toContain('WantedBy=multi-user.target');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('contains restart policy', () => {
|
|
136
|
+
const unit = generateSystemdUnit(
|
|
137
|
+
'/usr/bin/node',
|
|
138
|
+
'/home/user/nanoclaw',
|
|
139
|
+
'/home/user',
|
|
140
|
+
false,
|
|
141
|
+
);
|
|
142
|
+
expect(unit).toContain('Restart=always');
|
|
143
|
+
expect(unit).toContain('RestartSec=5');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('uses KillMode=process to preserve detached children', () => {
|
|
147
|
+
const unit = generateSystemdUnit(
|
|
148
|
+
'/usr/bin/node',
|
|
149
|
+
'/home/user/nanoclaw',
|
|
150
|
+
'/home/user',
|
|
151
|
+
false,
|
|
152
|
+
);
|
|
153
|
+
expect(unit).toContain('KillMode=process');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('sets correct ExecStart', () => {
|
|
157
|
+
const unit = generateSystemdUnit(
|
|
158
|
+
'/usr/bin/node',
|
|
159
|
+
'/srv/nanoclaw',
|
|
160
|
+
'/home/user',
|
|
161
|
+
false,
|
|
162
|
+
);
|
|
163
|
+
expect(unit).toContain(
|
|
164
|
+
'ExecStart=/usr/bin/node /srv/nanoclaw/dist/index.js',
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('WSL nohup fallback', () => {
|
|
170
|
+
it('generates a valid wrapper script', () => {
|
|
171
|
+
const projectRoot = '/home/user/nanoclaw';
|
|
172
|
+
const nodePath = '/usr/bin/node';
|
|
173
|
+
const pidFile = path.join(projectRoot, 'nanoclaw.pid');
|
|
174
|
+
|
|
175
|
+
// Simulate what service.ts generates
|
|
176
|
+
const wrapper = `#!/bin/bash
|
|
177
|
+
set -euo pipefail
|
|
178
|
+
cd ${JSON.stringify(projectRoot)}
|
|
179
|
+
nohup ${JSON.stringify(nodePath)} ${JSON.stringify(projectRoot)}/dist/index.js >> ${JSON.stringify(projectRoot)}/logs/nanoclaw.log 2>> ${JSON.stringify(projectRoot)}/logs/nanoclaw.error.log &
|
|
180
|
+
echo $! > ${JSON.stringify(pidFile)}`;
|
|
181
|
+
|
|
182
|
+
expect(wrapper).toContain('#!/bin/bash');
|
|
183
|
+
expect(wrapper).toContain('nohup');
|
|
184
|
+
expect(wrapper).toContain(nodePath);
|
|
185
|
+
expect(wrapper).toContain('nanoclaw.pid');
|
|
186
|
+
});
|
|
187
|
+
});
|