@rozek/nanoclaw 0.0.5 → 0.0.6

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.
Files changed (132) hide show
  1. package/container/agent-runner/package-lock.json +1524 -0
  2. package/dist/cli.js +39 -4
  3. package/dist/cli.js.map +1 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +34 -0
  6. package/dist/index.js.map +1 -1
  7. package/package.json +7 -1
  8. package/.claude/settings.json +0 -1
  9. package/.claude/skills/add-compact/SKILL.md +0 -135
  10. package/.claude/skills/add-discord/SKILL.md +0 -203
  11. package/.claude/skills/add-gmail/SKILL.md +0 -220
  12. package/.claude/skills/add-image-vision/SKILL.md +0 -94
  13. package/.claude/skills/add-ollama-tool/SKILL.md +0 -153
  14. package/.claude/skills/add-parallel/SKILL.md +0 -290
  15. package/.claude/skills/add-pdf-reader/SKILL.md +0 -104
  16. package/.claude/skills/add-reactions/SKILL.md +0 -117
  17. package/.claude/skills/add-slack/SKILL.md +0 -207
  18. package/.claude/skills/add-telegram/SKILL.md +0 -222
  19. package/.claude/skills/add-telegram-swarm/SKILL.md +0 -384
  20. package/.claude/skills/add-voice-transcription/SKILL.md +0 -148
  21. package/.claude/skills/add-whatsapp/SKILL.md +0 -372
  22. package/.claude/skills/convert-to-apple-container/SKILL.md +0 -175
  23. package/.claude/skills/customize/SKILL.md +0 -110
  24. package/.claude/skills/debug/SKILL.md +0 -349
  25. package/.claude/skills/get-qodo-rules/SKILL.md +0 -122
  26. package/.claude/skills/get-qodo-rules/references/output-format.md +0 -41
  27. package/.claude/skills/get-qodo-rules/references/pagination.md +0 -33
  28. package/.claude/skills/get-qodo-rules/references/repository-scope.md +0 -26
  29. package/.claude/skills/qodo-pr-resolver/SKILL.md +0 -326
  30. package/.claude/skills/qodo-pr-resolver/resources/providers.md +0 -329
  31. package/.claude/skills/setup/SKILL.md +0 -218
  32. package/.claude/skills/update-nanoclaw/SKILL.md +0 -235
  33. package/.claude/skills/update-skills/SKILL.md +0 -130
  34. package/.claude/skills/use-local-whisper/SKILL.md +0 -152
  35. package/.claude/skills/x-integration/SKILL.md +0 -417
  36. package/.claude/skills/x-integration/agent.ts +0 -243
  37. package/.claude/skills/x-integration/host.ts +0 -159
  38. package/.claude/skills/x-integration/lib/browser.ts +0 -148
  39. package/.claude/skills/x-integration/lib/config.ts +0 -62
  40. package/.claude/skills/x-integration/scripts/like.ts +0 -56
  41. package/.claude/skills/x-integration/scripts/post.ts +0 -66
  42. package/.claude/skills/x-integration/scripts/quote.ts +0 -80
  43. package/.claude/skills/x-integration/scripts/reply.ts +0 -74
  44. package/.claude/skills/x-integration/scripts/retweet.ts +0 -62
  45. package/.claude/skills/x-integration/scripts/setup.ts +0 -87
  46. package/.env.example +0 -1
  47. package/.github/CODEOWNERS +0 -10
  48. package/.github/PULL_REQUEST_TEMPLATE.md +0 -14
  49. package/.github/workflows/bump-version.yml +0 -32
  50. package/.github/workflows/ci.yml +0 -25
  51. package/.github/workflows/merge-forward-skills.yml +0 -160
  52. package/.github/workflows/update-tokens.yml +0 -42
  53. package/.husky/pre-commit +0 -1
  54. package/.mcp.json +0 -3
  55. package/.nvmrc +0 -1
  56. package/.prettierrc +0 -3
  57. package/CHANGELOG.md +0 -8
  58. package/CONTRIBUTING.md +0 -23
  59. package/CONTRIBUTORS.md +0 -15
  60. package/NanoClaw_with_Web-Support.md +0 -326
  61. package/README_zh.md +0 -200
  62. package/assets/nanoclaw-favicon.png +0 -0
  63. package/assets/nanoclaw-icon.png +0 -0
  64. package/assets/nanoclaw-logo-dark.png +0 -0
  65. package/assets/nanoclaw-logo.png +0 -0
  66. package/assets/nanoclaw-profile.jpeg +0 -0
  67. package/assets/nanoclaw-sales.png +0 -0
  68. package/assets/social-preview.jpg +0 -0
  69. package/config-examples/mount-allowlist.json +0 -25
  70. package/docs/APPLE-CONTAINER-NETWORKING.md +0 -90
  71. package/docs/DEBUG_CHECKLIST.md +0 -143
  72. package/docs/REQUIREMENTS.md +0 -196
  73. package/docs/SDK_DEEP_DIVE.md +0 -643
  74. package/docs/SECURITY.md +0 -122
  75. package/docs/SPEC.md +0 -785
  76. package/docs/docker-sandboxes.md +0 -359
  77. package/docs/nanoclaw-architecture-final.md +0 -1063
  78. package/docs/nanorepo-architecture.md +0 -168
  79. package/docs/skills-as-branches.md +0 -662
  80. package/groups/global/CLAUDE.md +0 -58
  81. package/groups/main/CLAUDE.md +0 -246
  82. package/launchd/com.nanoclaw.plist +0 -32
  83. package/repo-tokens/README.md +0 -113
  84. package/repo-tokens/action.yml +0 -186
  85. package/repo-tokens/badge.svg +0 -23
  86. package/repo-tokens/examples/green.svg +0 -14
  87. package/repo-tokens/examples/red.svg +0 -14
  88. package/repo-tokens/examples/yellow-green.svg +0 -14
  89. package/repo-tokens/examples/yellow.svg +0 -14
  90. package/scripts/run-migrations.ts +0 -105
  91. package/setup.sh +0 -161
  92. package/src/channels/index.ts +0 -15
  93. package/src/channels/registry.test.ts +0 -42
  94. package/src/channels/registry.ts +0 -32
  95. package/src/channels/web.ts +0 -1931
  96. package/src/cli.ts +0 -254
  97. package/src/config.ts +0 -73
  98. package/src/container-runner.test.ts +0 -210
  99. package/src/container-runner.ts +0 -768
  100. package/src/container-runtime.test.ts +0 -149
  101. package/src/container-runtime.ts +0 -127
  102. package/src/credential-proxy.test.ts +0 -192
  103. package/src/credential-proxy.ts +0 -125
  104. package/src/db.test.ts +0 -484
  105. package/src/db.ts +0 -803
  106. package/src/env.ts +0 -42
  107. package/src/formatting.test.ts +0 -256
  108. package/src/group-folder.test.ts +0 -43
  109. package/src/group-folder.ts +0 -44
  110. package/src/group-queue.test.ts +0 -484
  111. package/src/group-queue.ts +0 -379
  112. package/src/index.ts +0 -854
  113. package/src/ipc-auth.test.ts +0 -679
  114. package/src/ipc.ts +0 -461
  115. package/src/logger.ts +0 -16
  116. package/src/mount-security.ts +0 -419
  117. package/src/remote-control.test.ts +0 -397
  118. package/src/remote-control.ts +0 -224
  119. package/src/router.ts +0 -52
  120. package/src/routing.test.ts +0 -170
  121. package/src/sender-allowlist.test.ts +0 -216
  122. package/src/sender-allowlist.ts +0 -128
  123. package/src/session-commands.test.ts +0 -247
  124. package/src/session-commands.ts +0 -163
  125. package/src/task-scheduler.test.ts +0 -129
  126. package/src/task-scheduler.ts +0 -328
  127. package/src/timezone.test.ts +0 -29
  128. package/src/timezone.ts +0 -16
  129. package/src/types.ts +0 -109
  130. package/tsconfig.json +0 -20
  131. package/vitest.config.ts +0 -7
  132. package/vitest.skills.config.ts +0 -7
package/src/env.ts DELETED
@@ -1,42 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { logger } from './logger.js';
4
-
5
- /**
6
- * Parse the .env file and return values for the requested keys.
7
- * Does NOT load anything into process.env — callers decide what to
8
- * do with the values. This keeps secrets out of the process environment
9
- * so they don't leak to child processes.
10
- */
11
- export function readEnvFile(keys: string[]): Record<string, string> {
12
- const envFile = path.join(process.cwd(), '.env');
13
- let content: string;
14
- try {
15
- content = fs.readFileSync(envFile, 'utf-8');
16
- } catch (err) {
17
- logger.debug({ err }, '.env file not found, using defaults');
18
- return {};
19
- }
20
-
21
- const result: Record<string, string> = {};
22
- const wanted = new Set(keys);
23
-
24
- for (const line of content.split('\n')) {
25
- const trimmed = line.trim();
26
- if (!trimmed || trimmed.startsWith('#')) continue;
27
- const eqIdx = trimmed.indexOf('=');
28
- if (eqIdx === -1) continue;
29
- const key = trimmed.slice(0, eqIdx).trim();
30
- if (!wanted.has(key)) continue;
31
- let value = trimmed.slice(eqIdx + 1).trim();
32
- if (
33
- (value.startsWith('"') && value.endsWith('"')) ||
34
- (value.startsWith("'") && value.endsWith("'"))
35
- ) {
36
- value = value.slice(1, -1);
37
- }
38
- if (value) result[key] = value;
39
- }
40
-
41
- return result;
42
- }
@@ -1,256 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
-
3
- import { ASSISTANT_NAME, TRIGGER_PATTERN } from './config.js';
4
- import {
5
- escapeXml,
6
- formatMessages,
7
- formatOutbound,
8
- stripInternalTags,
9
- } from './router.js';
10
- import { NewMessage } from './types.js';
11
-
12
- function makeMsg(overrides: Partial<NewMessage> = {}): NewMessage {
13
- return {
14
- id: '1',
15
- chat_jid: 'group@g.us',
16
- sender: '123@s.whatsapp.net',
17
- sender_name: 'Alice',
18
- content: 'hello',
19
- timestamp: '2024-01-01T00:00:00.000Z',
20
- ...overrides,
21
- };
22
- }
23
-
24
- // --- escapeXml ---
25
-
26
- describe('escapeXml', () => {
27
- it('escapes ampersands', () => {
28
- expect(escapeXml('a & b')).toBe('a &amp; b');
29
- });
30
-
31
- it('escapes less-than', () => {
32
- expect(escapeXml('a < b')).toBe('a &lt; b');
33
- });
34
-
35
- it('escapes greater-than', () => {
36
- expect(escapeXml('a > b')).toBe('a &gt; b');
37
- });
38
-
39
- it('escapes double quotes', () => {
40
- expect(escapeXml('"hello"')).toBe('&quot;hello&quot;');
41
- });
42
-
43
- it('handles multiple special characters together', () => {
44
- expect(escapeXml('a & b < c > d "e"')).toBe(
45
- 'a &amp; b &lt; c &gt; d &quot;e&quot;',
46
- );
47
- });
48
-
49
- it('passes through strings with no special chars', () => {
50
- expect(escapeXml('hello world')).toBe('hello world');
51
- });
52
-
53
- it('handles empty string', () => {
54
- expect(escapeXml('')).toBe('');
55
- });
56
- });
57
-
58
- // --- formatMessages ---
59
-
60
- describe('formatMessages', () => {
61
- const TZ = 'UTC';
62
-
63
- it('formats a single message as XML with context header', () => {
64
- const result = formatMessages([makeMsg()], TZ);
65
- expect(result).toContain('<context timezone="UTC" />');
66
- expect(result).toContain('<message sender="Alice"');
67
- expect(result).toContain('>hello</message>');
68
- expect(result).toContain('Jan 1, 2024');
69
- });
70
-
71
- it('formats multiple messages', () => {
72
- const msgs = [
73
- makeMsg({
74
- id: '1',
75
- sender_name: 'Alice',
76
- content: 'hi',
77
- timestamp: '2024-01-01T00:00:00.000Z',
78
- }),
79
- makeMsg({
80
- id: '2',
81
- sender_name: 'Bob',
82
- content: 'hey',
83
- timestamp: '2024-01-01T01:00:00.000Z',
84
- }),
85
- ];
86
- const result = formatMessages(msgs, TZ);
87
- expect(result).toContain('sender="Alice"');
88
- expect(result).toContain('sender="Bob"');
89
- expect(result).toContain('>hi</message>');
90
- expect(result).toContain('>hey</message>');
91
- });
92
-
93
- it('escapes special characters in sender names', () => {
94
- const result = formatMessages([makeMsg({ sender_name: 'A & B <Co>' })], TZ);
95
- expect(result).toContain('sender="A &amp; B &lt;Co&gt;"');
96
- });
97
-
98
- it('escapes special characters in content', () => {
99
- const result = formatMessages(
100
- [makeMsg({ content: '<script>alert("xss")</script>' })],
101
- TZ,
102
- );
103
- expect(result).toContain(
104
- '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;',
105
- );
106
- });
107
-
108
- it('handles empty array', () => {
109
- const result = formatMessages([], TZ);
110
- expect(result).toContain('<context timezone="UTC" />');
111
- expect(result).toContain('<messages>\n\n</messages>');
112
- });
113
-
114
- it('converts timestamps to local time for given timezone', () => {
115
- // 2024-01-01T18:30:00Z in America/New_York (EST) = 1:30 PM
116
- const result = formatMessages(
117
- [makeMsg({ timestamp: '2024-01-01T18:30:00.000Z' })],
118
- 'America/New_York',
119
- );
120
- expect(result).toContain('1:30');
121
- expect(result).toContain('PM');
122
- expect(result).toContain('<context timezone="America/New_York" />');
123
- });
124
- });
125
-
126
- // --- TRIGGER_PATTERN ---
127
-
128
- describe('TRIGGER_PATTERN', () => {
129
- const name = ASSISTANT_NAME;
130
- const lower = name.toLowerCase();
131
- const upper = name.toUpperCase();
132
-
133
- it('matches @name at start of message', () => {
134
- expect(TRIGGER_PATTERN.test(`@${name} hello`)).toBe(true);
135
- });
136
-
137
- it('matches case-insensitively', () => {
138
- expect(TRIGGER_PATTERN.test(`@${lower} hello`)).toBe(true);
139
- expect(TRIGGER_PATTERN.test(`@${upper} hello`)).toBe(true);
140
- });
141
-
142
- it('does not match when not at start of message', () => {
143
- expect(TRIGGER_PATTERN.test(`hello @${name}`)).toBe(false);
144
- });
145
-
146
- it('does not match partial name like @NameExtra (word boundary)', () => {
147
- expect(TRIGGER_PATTERN.test(`@${name}extra hello`)).toBe(false);
148
- });
149
-
150
- it('matches with word boundary before apostrophe', () => {
151
- expect(TRIGGER_PATTERN.test(`@${name}'s thing`)).toBe(true);
152
- });
153
-
154
- it('matches @name alone (end of string is a word boundary)', () => {
155
- expect(TRIGGER_PATTERN.test(`@${name}`)).toBe(true);
156
- });
157
-
158
- it('matches with leading whitespace after trim', () => {
159
- // The actual usage trims before testing: TRIGGER_PATTERN.test(m.content.trim())
160
- expect(TRIGGER_PATTERN.test(`@${name} hey`.trim())).toBe(true);
161
- });
162
- });
163
-
164
- // --- Outbound formatting (internal tag stripping + prefix) ---
165
-
166
- describe('stripInternalTags', () => {
167
- it('strips single-line internal tags', () => {
168
- expect(stripInternalTags('hello <internal>secret</internal> world')).toBe(
169
- 'hello world',
170
- );
171
- });
172
-
173
- it('strips multi-line internal tags', () => {
174
- expect(
175
- stripInternalTags('hello <internal>\nsecret\nstuff\n</internal> world'),
176
- ).toBe('hello world');
177
- });
178
-
179
- it('strips multiple internal tag blocks', () => {
180
- expect(
181
- stripInternalTags('<internal>a</internal>hello<internal>b</internal>'),
182
- ).toBe('hello');
183
- });
184
-
185
- it('returns empty string when text is only internal tags', () => {
186
- expect(stripInternalTags('<internal>only this</internal>')).toBe('');
187
- });
188
- });
189
-
190
- describe('formatOutbound', () => {
191
- it('returns text with internal tags stripped', () => {
192
- expect(formatOutbound('hello world')).toBe('hello world');
193
- });
194
-
195
- it('returns empty string when all text is internal', () => {
196
- expect(formatOutbound('<internal>hidden</internal>')).toBe('');
197
- });
198
-
199
- it('strips internal tags from remaining text', () => {
200
- expect(
201
- formatOutbound('<internal>thinking</internal>The answer is 42'),
202
- ).toBe('The answer is 42');
203
- });
204
- });
205
-
206
- // --- Trigger gating with requiresTrigger flag ---
207
-
208
- describe('trigger gating (requiresTrigger interaction)', () => {
209
- // Replicates the exact logic from processGroupMessages and startMessageLoop:
210
- // if (!isMainGroup && group.requiresTrigger !== false) { check trigger }
211
- function shouldRequireTrigger(
212
- isMainGroup: boolean,
213
- requiresTrigger: boolean | undefined,
214
- ): boolean {
215
- return !isMainGroup && requiresTrigger !== false;
216
- }
217
-
218
- function shouldProcess(
219
- isMainGroup: boolean,
220
- requiresTrigger: boolean | undefined,
221
- messages: NewMessage[],
222
- ): boolean {
223
- if (!shouldRequireTrigger(isMainGroup, requiresTrigger)) return true;
224
- return messages.some((m) => TRIGGER_PATTERN.test(m.content.trim()));
225
- }
226
-
227
- it('main group always processes (no trigger needed)', () => {
228
- const msgs = [makeMsg({ content: 'hello no trigger' })];
229
- expect(shouldProcess(true, undefined, msgs)).toBe(true);
230
- });
231
-
232
- it('main group processes even with requiresTrigger=true', () => {
233
- const msgs = [makeMsg({ content: 'hello no trigger' })];
234
- expect(shouldProcess(true, true, msgs)).toBe(true);
235
- });
236
-
237
- it('non-main group with requiresTrigger=undefined requires trigger (defaults to true)', () => {
238
- const msgs = [makeMsg({ content: 'hello no trigger' })];
239
- expect(shouldProcess(false, undefined, msgs)).toBe(false);
240
- });
241
-
242
- it('non-main group with requiresTrigger=true requires trigger', () => {
243
- const msgs = [makeMsg({ content: 'hello no trigger' })];
244
- expect(shouldProcess(false, true, msgs)).toBe(false);
245
- });
246
-
247
- it('non-main group with requiresTrigger=true processes when trigger present', () => {
248
- const msgs = [makeMsg({ content: `@${ASSISTANT_NAME} do something` })];
249
- expect(shouldProcess(false, true, msgs)).toBe(true);
250
- });
251
-
252
- it('non-main group with requiresTrigger=false always processes (no trigger needed)', () => {
253
- const msgs = [makeMsg({ content: 'hello no trigger' })];
254
- expect(shouldProcess(false, false, msgs)).toBe(true);
255
- });
256
- });
@@ -1,43 +0,0 @@
1
- import path from 'path';
2
-
3
- import { describe, expect, it } from 'vitest';
4
-
5
- import {
6
- isValidGroupFolder,
7
- resolveGroupFolderPath,
8
- resolveGroupIpcPath,
9
- } from './group-folder.js';
10
-
11
- describe('group folder validation', () => {
12
- it('accepts normal group folder names', () => {
13
- expect(isValidGroupFolder('main')).toBe(true);
14
- expect(isValidGroupFolder('family-chat')).toBe(true);
15
- expect(isValidGroupFolder('Team_42')).toBe(true);
16
- });
17
-
18
- it('rejects traversal and reserved names', () => {
19
- expect(isValidGroupFolder('../../etc')).toBe(false);
20
- expect(isValidGroupFolder('/tmp')).toBe(false);
21
- expect(isValidGroupFolder('global')).toBe(false);
22
- expect(isValidGroupFolder('')).toBe(false);
23
- });
24
-
25
- it('resolves safe paths under groups directory', () => {
26
- const resolved = resolveGroupFolderPath('family-chat');
27
- expect(resolved.endsWith(`${path.sep}groups${path.sep}family-chat`)).toBe(
28
- true,
29
- );
30
- });
31
-
32
- it('resolves safe paths under data ipc directory', () => {
33
- const resolved = resolveGroupIpcPath('family-chat');
34
- expect(
35
- resolved.endsWith(`${path.sep}data${path.sep}ipc${path.sep}family-chat`),
36
- ).toBe(true);
37
- });
38
-
39
- it('throws for unsafe folder names', () => {
40
- expect(() => resolveGroupFolderPath('../../etc')).toThrow();
41
- expect(() => resolveGroupIpcPath('/tmp')).toThrow();
42
- });
43
- });
@@ -1,44 +0,0 @@
1
- import path from 'path';
2
-
3
- import { DATA_DIR, GROUPS_DIR } from './config.js';
4
-
5
- const GROUP_FOLDER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
6
- const RESERVED_FOLDERS = new Set(['global']);
7
-
8
- export function isValidGroupFolder(folder: string): boolean {
9
- if (!folder) return false;
10
- if (folder !== folder.trim()) return false;
11
- if (!GROUP_FOLDER_PATTERN.test(folder)) return false;
12
- if (folder.includes('/') || folder.includes('\\')) return false;
13
- if (folder.includes('..')) return false;
14
- if (RESERVED_FOLDERS.has(folder.toLowerCase())) return false;
15
- return true;
16
- }
17
-
18
- export function assertValidGroupFolder(folder: string): void {
19
- if (!isValidGroupFolder(folder)) {
20
- throw new Error(`Invalid group folder "${folder}"`);
21
- }
22
- }
23
-
24
- function ensureWithinBase(baseDir: string, resolvedPath: string): void {
25
- const rel = path.relative(baseDir, resolvedPath);
26
- if (rel.startsWith('..') || path.isAbsolute(rel)) {
27
- throw new Error(`Path escapes base directory: ${resolvedPath}`);
28
- }
29
- }
30
-
31
- export function resolveGroupFolderPath(folder: string): string {
32
- assertValidGroupFolder(folder);
33
- const groupPath = path.resolve(GROUPS_DIR, folder);
34
- ensureWithinBase(GROUPS_DIR, groupPath);
35
- return groupPath;
36
- }
37
-
38
- export function resolveGroupIpcPath(folder: string): string {
39
- assertValidGroupFolder(folder);
40
- const ipcBaseDir = path.resolve(DATA_DIR, 'ipc');
41
- const ipcPath = path.resolve(ipcBaseDir, folder);
42
- ensureWithinBase(ipcBaseDir, ipcPath);
43
- return ipcPath;
44
- }