@hybridaione/hybridclaw 0.2.2 → 0.2.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.
- package/.github/workflows/ci.yml +70 -0
- package/.husky/pre-commit +1 -0
- package/CHANGELOG.md +85 -0
- package/CONTRIBUTING.md +33 -0
- package/README.md +41 -16
- package/SECURITY.md +17 -0
- package/biome.json +35 -0
- package/config.example.json +71 -8
- package/container/package-lock.json +2 -2
- package/container/package.json +1 -1
- package/container/src/approval-policy.ts +1303 -0
- package/container/src/browser-tools.ts +431 -136
- package/container/src/extensions.ts +36 -12
- package/container/src/hybridai-client.ts +34 -13
- package/container/src/index.ts +451 -109
- package/container/src/ipc.ts +5 -3
- package/container/src/token-usage.ts +20 -10
- package/container/src/tools.ts +599 -225
- package/container/src/types.ts +32 -2
- package/container/src/web-fetch.ts +89 -32
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +10 -2
- package/dist/agent.js.map +1 -1
- package/dist/audit-cli.d.ts.map +1 -1
- package/dist/audit-cli.js +4 -2
- package/dist/audit-cli.js.map +1 -1
- package/dist/audit-events.d.ts.map +1 -1
- package/dist/audit-events.js +53 -3
- package/dist/audit-events.js.map +1 -1
- package/dist/audit-trail.d.ts.map +1 -1
- package/dist/audit-trail.js +17 -8
- package/dist/audit-trail.js.map +1 -1
- package/dist/channels/discord/attachments.d.ts.map +1 -1
- package/dist/channels/discord/attachments.js +14 -7
- package/dist/channels/discord/attachments.js.map +1 -1
- package/dist/channels/discord/debounce.d.ts +9 -0
- package/dist/channels/discord/debounce.d.ts.map +1 -0
- package/dist/channels/discord/debounce.js +20 -0
- package/dist/channels/discord/debounce.js.map +1 -0
- package/dist/channels/discord/delivery.d.ts +4 -1
- package/dist/channels/discord/delivery.d.ts.map +1 -1
- package/dist/channels/discord/delivery.js +19 -3
- package/dist/channels/discord/delivery.js.map +1 -1
- package/dist/channels/discord/human-delay.d.ts +16 -0
- package/dist/channels/discord/human-delay.d.ts.map +1 -0
- package/dist/channels/discord/human-delay.js +29 -0
- package/dist/channels/discord/human-delay.js.map +1 -0
- package/dist/channels/discord/inbound.d.ts +4 -0
- package/dist/channels/discord/inbound.d.ts.map +1 -1
- package/dist/channels/discord/inbound.js +45 -4
- package/dist/channels/discord/inbound.js.map +1 -1
- package/dist/channels/discord/mentions.d.ts.map +1 -1
- package/dist/channels/discord/mentions.js +16 -4
- package/dist/channels/discord/mentions.js.map +1 -1
- package/dist/channels/discord/presence.d.ts +33 -0
- package/dist/channels/discord/presence.d.ts.map +1 -0
- package/dist/channels/discord/presence.js +111 -0
- package/dist/channels/discord/presence.js.map +1 -0
- package/dist/channels/discord/rate-limiter.d.ts +14 -0
- package/dist/channels/discord/rate-limiter.d.ts.map +1 -0
- package/dist/channels/discord/rate-limiter.js +49 -0
- package/dist/channels/discord/rate-limiter.js.map +1 -0
- package/dist/channels/discord/reactions.d.ts +38 -0
- package/dist/channels/discord/reactions.d.ts.map +1 -0
- package/dist/channels/discord/reactions.js +151 -0
- package/dist/channels/discord/reactions.js.map +1 -0
- package/dist/channels/discord/runtime.d.ts +6 -3
- package/dist/channels/discord/runtime.d.ts.map +1 -1
- package/dist/channels/discord/runtime.js +621 -125
- package/dist/channels/discord/runtime.js.map +1 -1
- package/dist/channels/discord/stream.d.ts +4 -1
- package/dist/channels/discord/stream.d.ts.map +1 -1
- package/dist/channels/discord/stream.js +16 -8
- package/dist/channels/discord/stream.js.map +1 -1
- package/dist/channels/discord/tool-actions.d.ts.map +1 -1
- package/dist/channels/discord/tool-actions.js +24 -12
- package/dist/channels/discord/tool-actions.js.map +1 -1
- package/dist/channels/discord/typing.d.ts +15 -0
- package/dist/channels/discord/typing.d.ts.map +1 -0
- package/dist/channels/discord/typing.js +106 -0
- package/dist/channels/discord/typing.js.map +1 -0
- package/dist/chunk.d.ts.map +1 -1
- package/dist/chunk.js +4 -2
- package/dist/chunk.js.map +1 -1
- package/dist/cli.js +47 -22
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +103 -18
- package/dist/config.js.map +1 -1
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js +58 -26
- package/dist/container-runner.js.map +1 -1
- package/dist/container-setup.d.ts.map +1 -1
- package/dist/container-setup.js +10 -9
- package/dist/container-setup.js.map +1 -1
- package/dist/conversation.d.ts +2 -2
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +1 -1
- package/dist/conversation.js.map +1 -1
- package/dist/db.d.ts +118 -2
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +1568 -50
- package/dist/db.js.map +1 -1
- package/dist/delegation-manager.d.ts.map +1 -1
- package/dist/delegation-manager.js +3 -2
- package/dist/delegation-manager.js.map +1 -1
- package/dist/gateway-client.d.ts +2 -2
- package/dist/gateway-client.d.ts.map +1 -1
- package/dist/gateway-client.js +10 -4
- package/dist/gateway-client.js.map +1 -1
- package/dist/gateway-service.d.ts +3 -3
- package/dist/gateway-service.d.ts.map +1 -1
- package/dist/gateway-service.js +563 -73
- package/dist/gateway-service.js.map +1 -1
- package/dist/gateway-types.d.ts +24 -0
- package/dist/gateway-types.d.ts.map +1 -1
- package/dist/gateway-types.js.map +1 -1
- package/dist/gateway.js +179 -24
- package/dist/gateway.js.map +1 -1
- package/dist/health.d.ts.map +1 -1
- package/dist/health.js +20 -10
- package/dist/health.js.map +1 -1
- package/dist/heartbeat.d.ts +4 -0
- package/dist/heartbeat.d.ts.map +1 -1
- package/dist/heartbeat.js +48 -20
- package/dist/heartbeat.js.map +1 -1
- package/dist/hybridai-bots.d.ts.map +1 -1
- package/dist/hybridai-bots.js +4 -2
- package/dist/hybridai-bots.js.map +1 -1
- package/dist/instruction-approval-audit.d.ts.map +1 -1
- package/dist/instruction-approval-audit.js.map +1 -1
- package/dist/instruction-integrity.d.ts.map +1 -1
- package/dist/instruction-integrity.js +8 -2
- package/dist/instruction-integrity.js.map +1 -1
- package/dist/ipc.d.ts.map +1 -1
- package/dist/ipc.js +6 -1
- package/dist/ipc.js.map +1 -1
- package/dist/logger.js.map +1 -1
- package/dist/memory-consolidation.d.ts +17 -0
- package/dist/memory-consolidation.d.ts.map +1 -0
- package/dist/memory-consolidation.js +25 -0
- package/dist/memory-consolidation.js.map +1 -0
- package/dist/memory-service.d.ts +200 -0
- package/dist/memory-service.d.ts.map +1 -0
- package/dist/memory-service.js +294 -0
- package/dist/memory-service.js.map +1 -0
- package/dist/mount-security.d.ts.map +1 -1
- package/dist/mount-security.js +31 -7
- package/dist/mount-security.js.map +1 -1
- package/dist/observability-ingest.d.ts.map +1 -1
- package/dist/observability-ingest.js +32 -11
- package/dist/observability-ingest.js.map +1 -1
- package/dist/onboarding.d.ts.map +1 -1
- package/dist/onboarding.js +32 -9
- package/dist/onboarding.js.map +1 -1
- package/dist/proactive-policy.d.ts.map +1 -1
- package/dist/proactive-policy.js +2 -1
- package/dist/proactive-policy.js.map +1 -1
- package/dist/prompt-hooks.d.ts.map +1 -1
- package/dist/prompt-hooks.js +9 -7
- package/dist/prompt-hooks.js.map +1 -1
- package/dist/runtime-config.d.ts +98 -1
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +477 -23
- package/dist/runtime-config.js.map +1 -1
- package/dist/scheduled-task-runner.d.ts +1 -0
- package/dist/scheduled-task-runner.d.ts.map +1 -1
- package/dist/scheduled-task-runner.js +29 -10
- package/dist/scheduled-task-runner.js.map +1 -1
- package/dist/scheduler.d.ts +43 -4
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +530 -56
- package/dist/scheduler.js.map +1 -1
- package/dist/session-export.d.ts +26 -0
- package/dist/session-export.d.ts.map +1 -0
- package/dist/session-export.js +149 -0
- package/dist/session-export.js.map +1 -0
- package/dist/session-maintenance.d.ts.map +1 -1
- package/dist/session-maintenance.js +75 -13
- package/dist/session-maintenance.js.map +1 -1
- package/dist/session-transcripts.d.ts.map +1 -1
- package/dist/session-transcripts.js.map +1 -1
- package/dist/side-effects.d.ts.map +1 -1
- package/dist/side-effects.js +14 -2
- package/dist/side-effects.js.map +1 -1
- package/dist/skills-guard.d.ts.map +1 -1
- package/dist/skills-guard.js +893 -130
- package/dist/skills-guard.js.map +1 -1
- package/dist/skills.d.ts +5 -0
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +29 -15
- package/dist/skills.js.map +1 -1
- package/dist/token-efficiency.d.ts.map +1 -1
- package/dist/token-efficiency.js.map +1 -1
- package/dist/tui.js +92 -11
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts +146 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +24 -1
- package/dist/types.js.map +1 -1
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +42 -14
- package/dist/update.js.map +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +49 -9
- package/dist/workspace.js.map +1 -1
- package/docs/chat.html +9 -3
- package/docs/index.html +37 -13
- package/package.json +8 -2
- package/src/agent.ts +16 -3
- package/src/audit-cli.ts +44 -16
- package/src/audit-events.ts +69 -5
- package/src/audit-trail.ts +41 -15
- package/src/channels/discord/attachments.ts +81 -27
- package/src/channels/discord/debounce.ts +25 -0
- package/src/channels/discord/delivery.ts +57 -13
- package/src/channels/discord/human-delay.ts +48 -0
- package/src/channels/discord/inbound.ts +66 -7
- package/src/channels/discord/mentions.ts +42 -18
- package/src/channels/discord/presence.ts +148 -0
- package/src/channels/discord/rate-limiter.ts +58 -0
- package/src/channels/discord/reactions.ts +211 -0
- package/src/channels/discord/runtime.ts +1048 -182
- package/src/channels/discord/stream.ts +73 -27
- package/src/channels/discord/tool-actions.ts +78 -37
- package/src/channels/discord/typing.ts +140 -0
- package/src/chunk.ts +12 -4
- package/src/cli.ts +141 -56
- package/src/config.ts +192 -34
- package/src/container-runner.ts +132 -42
- package/src/container-setup.ts +57 -22
- package/src/conversation.ts +9 -7
- package/src/db.ts +2217 -84
- package/src/delegation-manager.ts +6 -2
- package/src/gateway-client.ts +41 -17
- package/src/gateway-service.ts +1019 -201
- package/src/gateway-types.ts +33 -0
- package/src/gateway.ts +321 -48
- package/src/health.ts +66 -26
- package/src/heartbeat.ts +84 -22
- package/src/hybridai-bots.ts +14 -5
- package/src/instruction-approval-audit.ts +4 -1
- package/src/instruction-integrity.ts +30 -9
- package/src/ipc.ts +23 -5
- package/src/logger.ts +4 -1
- package/src/memory-consolidation.ts +41 -0
- package/src/memory-service.ts +606 -0
- package/src/mount-security.ts +58 -13
- package/src/observability-ingest.ts +134 -35
- package/src/onboarding.ts +126 -35
- package/src/proactive-policy.ts +3 -1
- package/src/prompt-hooks.ts +40 -17
- package/src/runtime-config.ts +1114 -99
- package/src/scheduled-task-runner.ts +63 -11
- package/src/scheduler.ts +683 -60
- package/src/session-export.ts +196 -0
- package/src/session-maintenance.ts +125 -22
- package/src/session-transcripts.ts +12 -3
- package/src/side-effects.ts +28 -5
- package/src/skills-guard.ts +1067 -219
- package/src/skills.ts +163 -65
- package/src/token-efficiency.ts +31 -9
- package/src/tui.ts +166 -25
- package/src/types.ts +195 -2
- package/src/update.ts +79 -23
- package/src/workspace.ts +63 -11
- package/tests/approval-policy.test.ts +224 -0
- package/tests/discord.basic.test.ts +82 -2
- package/tests/discord.human-presence.test.ts +85 -0
- package/tests/gateway-service.media-routing.test.ts +8 -2
- package/tests/memory-service.test.ts +1114 -0
- package/tests/token-efficiency.basic.test.ts +8 -2
- package/vitest.e2e.config.ts +3 -1
- package/vitest.integration.config.ts +3 -1
- package/vitest.live.config.ts +3 -1
- package/vitest.unit.config.ts +9 -0
package/src/update.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'child_process';
|
|
1
2
|
import fs from 'fs';
|
|
2
3
|
import path from 'path';
|
|
3
|
-
import { spawn, spawnSync } from 'child_process';
|
|
4
4
|
import readline from 'readline/promises';
|
|
5
5
|
|
|
6
6
|
const DEFAULT_PACKAGE_NAME = '@hybridaione/hybridclaw';
|
|
@@ -52,8 +52,14 @@ function readPackageInfo(packageJsonPath: string): PackageInfo {
|
|
|
52
52
|
try {
|
|
53
53
|
const raw = fs.readFileSync(packageJsonPath, 'utf-8');
|
|
54
54
|
const parsed = JSON.parse(raw) as PackageManifest;
|
|
55
|
-
const name =
|
|
56
|
-
|
|
55
|
+
const name =
|
|
56
|
+
typeof parsed.name === 'string' && parsed.name.trim()
|
|
57
|
+
? parsed.name.trim()
|
|
58
|
+
: null;
|
|
59
|
+
const version =
|
|
60
|
+
typeof parsed.version === 'string' && parsed.version.trim()
|
|
61
|
+
? parsed.version.trim()
|
|
62
|
+
: null;
|
|
57
63
|
return { name, version };
|
|
58
64
|
} catch {
|
|
59
65
|
return { name: null, version: null };
|
|
@@ -88,9 +94,10 @@ function findNearestPackageRoot(startPath: string | undefined): string | null {
|
|
|
88
94
|
let current: string;
|
|
89
95
|
try {
|
|
90
96
|
const resolved = path.resolve(startPath);
|
|
91
|
-
current =
|
|
92
|
-
|
|
93
|
-
|
|
97
|
+
current =
|
|
98
|
+
fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()
|
|
99
|
+
? resolved
|
|
100
|
+
: path.dirname(resolved);
|
|
94
101
|
} catch {
|
|
95
102
|
return null;
|
|
96
103
|
}
|
|
@@ -134,14 +141,24 @@ function detectPackageManager(): PackageManager {
|
|
|
134
141
|
return 'npm';
|
|
135
142
|
}
|
|
136
143
|
|
|
137
|
-
function detectInstallContext(
|
|
144
|
+
function detectInstallContext(
|
|
145
|
+
packageName: string,
|
|
146
|
+
entryPath: string | undefined,
|
|
147
|
+
): InstallContext {
|
|
138
148
|
const preferredManager = detectPackageManager();
|
|
139
149
|
const entryRoot = findNearestPackageRoot(entryPath);
|
|
140
150
|
const cwdRoot = findNearestPackageRoot(process.cwd());
|
|
141
151
|
const cwdInfo = readPackageInfo(path.join(process.cwd(), 'package.json'));
|
|
142
152
|
|
|
143
|
-
if (
|
|
144
|
-
|
|
153
|
+
if (
|
|
154
|
+
cwdInfo.name === packageName &&
|
|
155
|
+
fs.existsSync(path.join(process.cwd(), '.git'))
|
|
156
|
+
) {
|
|
157
|
+
return {
|
|
158
|
+
kind: 'source',
|
|
159
|
+
root: process.cwd(),
|
|
160
|
+
packageManager: preferredManager,
|
|
161
|
+
};
|
|
145
162
|
}
|
|
146
163
|
|
|
147
164
|
if (!entryRoot) {
|
|
@@ -157,15 +174,27 @@ function detectInstallContext(packageName: string, entryPath: string | undefined
|
|
|
157
174
|
packageManager: preferredManager,
|
|
158
175
|
};
|
|
159
176
|
}
|
|
160
|
-
return {
|
|
177
|
+
return {
|
|
178
|
+
kind: 'unknown',
|
|
179
|
+
root: entryRoot,
|
|
180
|
+
packageManager: preferredManager,
|
|
181
|
+
};
|
|
161
182
|
}
|
|
162
183
|
|
|
163
184
|
if (fs.existsSync(path.join(entryRoot, '.git'))) {
|
|
164
|
-
return {
|
|
185
|
+
return {
|
|
186
|
+
kind: 'source',
|
|
187
|
+
root: entryRoot,
|
|
188
|
+
packageManager: preferredManager,
|
|
189
|
+
};
|
|
165
190
|
}
|
|
166
191
|
|
|
167
192
|
if (entryRoot.includes(`${path.sep}node_modules${path.sep}`)) {
|
|
168
|
-
return {
|
|
193
|
+
return {
|
|
194
|
+
kind: 'package',
|
|
195
|
+
root: entryRoot,
|
|
196
|
+
packageManager: preferredManager,
|
|
197
|
+
};
|
|
169
198
|
}
|
|
170
199
|
|
|
171
200
|
return { kind: 'unknown', root: entryRoot, packageManager: preferredManager };
|
|
@@ -179,7 +208,12 @@ function parseSemver(value: string): ParsedSemver | null {
|
|
|
179
208
|
const major = Number.parseInt(match[1], 10);
|
|
180
209
|
const minor = Number.parseInt(match[2], 10);
|
|
181
210
|
const patch = Number.parseInt(match[3], 10);
|
|
182
|
-
if (
|
|
211
|
+
if (
|
|
212
|
+
!Number.isFinite(major) ||
|
|
213
|
+
!Number.isFinite(minor) ||
|
|
214
|
+
!Number.isFinite(patch)
|
|
215
|
+
)
|
|
216
|
+
return null;
|
|
183
217
|
|
|
184
218
|
return {
|
|
185
219
|
major,
|
|
@@ -239,7 +273,9 @@ function commandAvailable(command: string): boolean {
|
|
|
239
273
|
return result.status === 0;
|
|
240
274
|
}
|
|
241
275
|
|
|
242
|
-
function resolveAvailablePackageManager(
|
|
276
|
+
function resolveAvailablePackageManager(
|
|
277
|
+
preferred: PackageManager,
|
|
278
|
+
): PackageManager | null {
|
|
243
279
|
const order: PackageManager[] = [preferred, 'npm', 'pnpm', 'yarn', 'bun'];
|
|
244
280
|
const checked = new Set<PackageManager>();
|
|
245
281
|
for (const candidate of order) {
|
|
@@ -250,7 +286,10 @@ function resolveAvailablePackageManager(preferred: PackageManager): PackageManag
|
|
|
250
286
|
return null;
|
|
251
287
|
}
|
|
252
288
|
|
|
253
|
-
function buildUpdateCommand(
|
|
289
|
+
function buildUpdateCommand(
|
|
290
|
+
packageManager: PackageManager,
|
|
291
|
+
packageName: string,
|
|
292
|
+
): UpdateCommand {
|
|
254
293
|
switch (packageManager) {
|
|
255
294
|
case 'pnpm': {
|
|
256
295
|
const args = ['add', '-g', `${packageName}@latest`];
|
|
@@ -277,7 +316,9 @@ async function askForConfirmation(message: string): Promise<boolean> {
|
|
|
277
316
|
output: process.stdout,
|
|
278
317
|
});
|
|
279
318
|
try {
|
|
280
|
-
const answer = (await rl.question(`${message} [y/N] `))
|
|
319
|
+
const answer = (await rl.question(`${message} [y/N] `))
|
|
320
|
+
.trim()
|
|
321
|
+
.toLowerCase();
|
|
281
322
|
return answer === 'y' || answer === 'yes';
|
|
282
323
|
} finally {
|
|
283
324
|
rl.close();
|
|
@@ -304,7 +345,10 @@ Options:
|
|
|
304
345
|
--yes, -y Skip confirmation prompt before install`);
|
|
305
346
|
}
|
|
306
347
|
|
|
307
|
-
export async function runUpdateCommand(
|
|
348
|
+
export async function runUpdateCommand(
|
|
349
|
+
args: string[],
|
|
350
|
+
currentVersion: string,
|
|
351
|
+
): Promise<void> {
|
|
308
352
|
const options = parseUpdateArgs(args);
|
|
309
353
|
if (options.help) {
|
|
310
354
|
printUpdateUsage();
|
|
@@ -314,7 +358,9 @@ export async function runUpdateCommand(args: string[], currentVersion: string):
|
|
|
314
358
|
const packageName = resolvePackageName(process.argv[1]);
|
|
315
359
|
const install = detectInstallContext(packageName, process.argv[1]);
|
|
316
360
|
const latest = fetchLatestVersion(packageName);
|
|
317
|
-
const comparison = latest.version
|
|
361
|
+
const comparison = latest.version
|
|
362
|
+
? compareSemver(currentVersion, latest.version)
|
|
363
|
+
: null;
|
|
318
364
|
|
|
319
365
|
console.log(`Current version: ${currentVersion}`);
|
|
320
366
|
if (latest.version) {
|
|
@@ -325,7 +371,9 @@ export async function runUpdateCommand(args: string[], currentVersion: string):
|
|
|
325
371
|
|
|
326
372
|
if (install.kind === 'source') {
|
|
327
373
|
console.log('');
|
|
328
|
-
console.log(
|
|
374
|
+
console.log(
|
|
375
|
+
`Source checkout detected at ${install.root || process.cwd()}.`,
|
|
376
|
+
);
|
|
329
377
|
console.log('To update, run:');
|
|
330
378
|
console.log(' git pull --rebase');
|
|
331
379
|
console.log(' npm install');
|
|
@@ -342,14 +390,20 @@ export async function runUpdateCommand(args: string[], currentVersion: string):
|
|
|
342
390
|
} else if (latest.version && comparison === 0) {
|
|
343
391
|
console.log('HybridClaw is already up to date.');
|
|
344
392
|
} else if (latest.version && comparison === 1) {
|
|
345
|
-
console.log(
|
|
393
|
+
console.log(
|
|
394
|
+
'Installed version is newer than npm latest; skipping automatic update.',
|
|
395
|
+
);
|
|
346
396
|
} else if (latest.version) {
|
|
347
|
-
console.log(
|
|
397
|
+
console.log(
|
|
398
|
+
'Version comparison unavailable; semver format not recognized.',
|
|
399
|
+
);
|
|
348
400
|
}
|
|
349
401
|
|
|
350
402
|
const manager = resolveAvailablePackageManager(install.packageManager);
|
|
351
403
|
if (!manager) {
|
|
352
|
-
throw new Error(
|
|
404
|
+
throw new Error(
|
|
405
|
+
'No supported package manager found (npm, pnpm, yarn, bun).',
|
|
406
|
+
);
|
|
353
407
|
}
|
|
354
408
|
const updateCommand = buildUpdateCommand(manager, packageName);
|
|
355
409
|
|
|
@@ -370,7 +424,9 @@ export async function runUpdateCommand(args: string[], currentVersion: string):
|
|
|
370
424
|
console.log(`Update command: ${updateCommand.display}`);
|
|
371
425
|
if (!options.yes) {
|
|
372
426
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
373
|
-
console.log(
|
|
427
|
+
console.log(
|
|
428
|
+
'Non-interactive shell detected. Re-run with `--yes` to apply the update.',
|
|
429
|
+
);
|
|
374
430
|
return;
|
|
375
431
|
}
|
|
376
432
|
const confirmed = await askForConfirmation('Proceed with update now?');
|
package/src/workspace.ts
CHANGED
|
@@ -3,11 +3,10 @@
|
|
|
3
3
|
* TOOLS.md, MEMORY.md, HEARTBEAT.md from the agent workspace
|
|
4
4
|
* and injects them into the system prompt (like OpenClaw).
|
|
5
5
|
*/
|
|
6
|
-
import fs from 'fs';
|
|
7
|
-
import path from 'path';
|
|
8
|
-
|
|
9
|
-
import { logger } from './logger.js';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
10
8
|
import { agentWorkspaceDir } from './ipc.js';
|
|
9
|
+
import { logger } from './logger.js';
|
|
11
10
|
import { truncateHeadTailText } from './token-efficiency.js';
|
|
12
11
|
|
|
13
12
|
const BOOTSTRAP_FILES = [
|
|
@@ -21,6 +20,21 @@ const BOOTSTRAP_FILES = [
|
|
|
21
20
|
'BOOTSTRAP.md',
|
|
22
21
|
'BOOT.md',
|
|
23
22
|
] as const;
|
|
23
|
+
const POLICY_RELATIVE_PATH = path.join('.hybridclaw', 'policy.yaml');
|
|
24
|
+
const DEFAULT_POLICY_TEMPLATE = `approval:
|
|
25
|
+
pinned_red:
|
|
26
|
+
- pattern: "rm -rf /"
|
|
27
|
+
- paths: ["~/.ssh/**", "/etc/**", ".env*"]
|
|
28
|
+
- tools: ["force_push"]
|
|
29
|
+
|
|
30
|
+
workspace_fence: true
|
|
31
|
+
max_pending_approvals: 3
|
|
32
|
+
approval_timeout_secs: 120
|
|
33
|
+
|
|
34
|
+
audit:
|
|
35
|
+
log_all_red: true
|
|
36
|
+
log_denials: true
|
|
37
|
+
`;
|
|
24
38
|
|
|
25
39
|
const MAX_FILE_CHARS = 20_000;
|
|
26
40
|
const TEMPLATES_DIR = path.join(process.cwd(), 'templates');
|
|
@@ -47,6 +61,25 @@ export function ensureBootstrapFiles(agentId: string): void {
|
|
|
47
61
|
logger.debug({ agentId, file: filename }, 'Copied bootstrap template');
|
|
48
62
|
}
|
|
49
63
|
}
|
|
64
|
+
|
|
65
|
+
const policyDestPath = path.join(wsDir, POLICY_RELATIVE_PATH);
|
|
66
|
+
if (!fs.existsSync(policyDestPath)) {
|
|
67
|
+
fs.mkdirSync(path.dirname(policyDestPath), { recursive: true });
|
|
68
|
+
const repoPolicyPath = path.join(process.cwd(), POLICY_RELATIVE_PATH);
|
|
69
|
+
if (fs.existsSync(repoPolicyPath)) {
|
|
70
|
+
fs.copyFileSync(repoPolicyPath, policyDestPath);
|
|
71
|
+
logger.debug(
|
|
72
|
+
{ agentId, file: POLICY_RELATIVE_PATH },
|
|
73
|
+
'Copied approval policy from repository',
|
|
74
|
+
);
|
|
75
|
+
} else {
|
|
76
|
+
fs.writeFileSync(policyDestPath, DEFAULT_POLICY_TEMPLATE, 'utf-8');
|
|
77
|
+
logger.debug(
|
|
78
|
+
{ agentId, file: POLICY_RELATIVE_PATH },
|
|
79
|
+
'Wrote default approval policy template',
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
50
83
|
}
|
|
51
84
|
|
|
52
85
|
/**
|
|
@@ -71,7 +104,10 @@ export function loadBootstrapFiles(agentId: string): ContextFile[] {
|
|
|
71
104
|
|
|
72
105
|
files.push({ name: filename, content });
|
|
73
106
|
} catch (err) {
|
|
74
|
-
logger.warn(
|
|
107
|
+
logger.warn(
|
|
108
|
+
{ agentId, file: filename, err },
|
|
109
|
+
'Failed to read bootstrap file',
|
|
110
|
+
);
|
|
75
111
|
}
|
|
76
112
|
}
|
|
77
113
|
|
|
@@ -83,7 +119,10 @@ export function loadBootstrapFiles(agentId: string): ContextFile[] {
|
|
|
83
119
|
* e.g. "Tuesday, February 24th, 2026 — 14:32"
|
|
84
120
|
*/
|
|
85
121
|
function formatCurrentTime(timezone?: string): string {
|
|
86
|
-
const tz =
|
|
122
|
+
const tz =
|
|
123
|
+
timezone?.trim() ||
|
|
124
|
+
Intl.DateTimeFormat().resolvedOptions().timeZone ||
|
|
125
|
+
'UTC';
|
|
87
126
|
const now = new Date();
|
|
88
127
|
try {
|
|
89
128
|
const parts = new Intl.DateTimeFormat('en-US', {
|
|
@@ -100,14 +139,27 @@ function formatCurrentTime(timezone?: string): string {
|
|
|
100
139
|
for (const part of parts) {
|
|
101
140
|
if (part.type !== 'literal') map[part.type] = part.value;
|
|
102
141
|
}
|
|
103
|
-
if (
|
|
142
|
+
if (
|
|
143
|
+
!map.weekday ||
|
|
144
|
+
!map.year ||
|
|
145
|
+
!map.month ||
|
|
146
|
+
!map.day ||
|
|
147
|
+
!map.hour ||
|
|
148
|
+
!map.minute
|
|
149
|
+
) {
|
|
104
150
|
return now.toISOString();
|
|
105
151
|
}
|
|
106
152
|
const dayNum = parseInt(map.day, 10);
|
|
107
|
-
const suffix =
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
153
|
+
const suffix =
|
|
154
|
+
dayNum >= 11 && dayNum <= 13
|
|
155
|
+
? 'th'
|
|
156
|
+
: dayNum % 10 === 1
|
|
157
|
+
? 'st'
|
|
158
|
+
: dayNum % 10 === 2
|
|
159
|
+
? 'nd'
|
|
160
|
+
: dayNum % 10 === 3
|
|
161
|
+
? 'rd'
|
|
162
|
+
: 'th';
|
|
111
163
|
return `${map.weekday}, ${map.month} ${dayNum}${suffix}, ${map.year} — ${map.hour}:${map.minute} (${tz})`;
|
|
112
164
|
} catch {
|
|
113
165
|
return now.toISOString();
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { describe, expect, test } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { TrustedCoworkerApprovalRuntime } from '../container/src/approval-policy.js';
|
|
7
|
+
import type { ChatMessage } from '../container/src/types.js';
|
|
8
|
+
|
|
9
|
+
function userMessage(text: string): ChatMessage {
|
|
10
|
+
return { role: 'user', content: text };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function tempTrustStorePath(name: string): string {
|
|
14
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hybridclaw-approval-'));
|
|
15
|
+
return path.join(dir, `${name}.json`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('TrustedCoworkerApprovalRuntime', () => {
|
|
19
|
+
test('yellow actions promote to green after successful repeat', () => {
|
|
20
|
+
const runtime = new TrustedCoworkerApprovalRuntime(
|
|
21
|
+
'/tmp/hybridclaw-missing-policy.yaml',
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const first = runtime.evaluateToolCall({
|
|
25
|
+
toolName: 'bash',
|
|
26
|
+
argsJson: JSON.stringify({ command: 'npm install' }),
|
|
27
|
+
latestUserPrompt: 'Install dependencies',
|
|
28
|
+
});
|
|
29
|
+
expect(first.tier).toBe('yellow');
|
|
30
|
+
expect(first.decision).toBe('implicit');
|
|
31
|
+
|
|
32
|
+
runtime.afterToolExecution(first, true);
|
|
33
|
+
|
|
34
|
+
const second = runtime.evaluateToolCall({
|
|
35
|
+
toolName: 'bash',
|
|
36
|
+
argsJson: JSON.stringify({ command: 'npm install' }),
|
|
37
|
+
latestUserPrompt: 'Install dependencies',
|
|
38
|
+
});
|
|
39
|
+
expect(second.tier).toBe('green');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('sensitive paths stay pinned red and require explicit approval', () => {
|
|
43
|
+
const runtime = new TrustedCoworkerApprovalRuntime(
|
|
44
|
+
'/tmp/hybridclaw-missing-policy.yaml',
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const evaluation = runtime.evaluateToolCall({
|
|
48
|
+
toolName: 'write',
|
|
49
|
+
argsJson: JSON.stringify({ path: '.env', contents: 'API_KEY=abc' }),
|
|
50
|
+
latestUserPrompt: 'Write env file',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(evaluation.baseTier).toBe('red');
|
|
54
|
+
expect(evaluation.decision).toBe('required');
|
|
55
|
+
expect(evaluation.pinned).toBe(true);
|
|
56
|
+
expect(evaluation.requestId).toBeTruthy();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('yes response approves once and replays original prompt', () => {
|
|
60
|
+
const runtime = new TrustedCoworkerApprovalRuntime(
|
|
61
|
+
'/tmp/hybridclaw-missing-policy.yaml',
|
|
62
|
+
);
|
|
63
|
+
const originalPrompt = 'Delete dist and rebuild cleanly';
|
|
64
|
+
|
|
65
|
+
const pending = runtime.evaluateToolCall({
|
|
66
|
+
toolName: 'bash',
|
|
67
|
+
argsJson: JSON.stringify({ command: 'rm -rf dist' }),
|
|
68
|
+
latestUserPrompt: originalPrompt,
|
|
69
|
+
});
|
|
70
|
+
expect(pending.decision).toBe('required');
|
|
71
|
+
|
|
72
|
+
const prelude = runtime.handleApprovalResponse([userMessage('yes')]);
|
|
73
|
+
expect(prelude?.replayPrompt).toBe(originalPrompt);
|
|
74
|
+
expect(prelude?.approvalMode).toBe('once');
|
|
75
|
+
|
|
76
|
+
const approved = runtime.evaluateToolCall({
|
|
77
|
+
toolName: 'bash',
|
|
78
|
+
argsJson: JSON.stringify({ command: 'rm -rf dist' }),
|
|
79
|
+
latestUserPrompt: originalPrompt,
|
|
80
|
+
});
|
|
81
|
+
expect(approved.decision).toBe('approved_once');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('yes for session persists trust for repeated action key', () => {
|
|
85
|
+
const runtime = new TrustedCoworkerApprovalRuntime(
|
|
86
|
+
'/tmp/hybridclaw-missing-policy.yaml',
|
|
87
|
+
);
|
|
88
|
+
const originalPrompt = 'Fetch from example.com';
|
|
89
|
+
|
|
90
|
+
const first = runtime.evaluateToolCall({
|
|
91
|
+
toolName: 'web_fetch',
|
|
92
|
+
argsJson: JSON.stringify({ url: 'https://example.com' }),
|
|
93
|
+
latestUserPrompt: originalPrompt,
|
|
94
|
+
});
|
|
95
|
+
expect(first.decision).toBe('required');
|
|
96
|
+
|
|
97
|
+
const prelude = runtime.handleApprovalResponse([
|
|
98
|
+
userMessage('yes for session'),
|
|
99
|
+
]);
|
|
100
|
+
expect(prelude?.approvalMode).toBe('session');
|
|
101
|
+
|
|
102
|
+
const second = runtime.evaluateToolCall({
|
|
103
|
+
toolName: 'web_fetch',
|
|
104
|
+
argsJson: JSON.stringify({ url: 'https://example.com' }),
|
|
105
|
+
latestUserPrompt: originalPrompt,
|
|
106
|
+
});
|
|
107
|
+
expect(second.decision).toBe('approved_session');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('pinned red cannot be session-trusted across runs', () => {
|
|
111
|
+
const runtime = new TrustedCoworkerApprovalRuntime(
|
|
112
|
+
'/tmp/hybridclaw-missing-policy.yaml',
|
|
113
|
+
);
|
|
114
|
+
const prompt = 'Append token to .env';
|
|
115
|
+
|
|
116
|
+
const first = runtime.evaluateToolCall({
|
|
117
|
+
toolName: 'write',
|
|
118
|
+
argsJson: JSON.stringify({ path: '.env', contents: 'TOKEN=x' }),
|
|
119
|
+
latestUserPrompt: prompt,
|
|
120
|
+
});
|
|
121
|
+
expect(first.decision).toBe('required');
|
|
122
|
+
expect(first.pinned).toBe(true);
|
|
123
|
+
|
|
124
|
+
const prelude = runtime.handleApprovalResponse([
|
|
125
|
+
userMessage('yes for session'),
|
|
126
|
+
]);
|
|
127
|
+
expect(prelude?.approvalMode).toBe('once');
|
|
128
|
+
|
|
129
|
+
const second = runtime.evaluateToolCall({
|
|
130
|
+
toolName: 'write',
|
|
131
|
+
argsJson: JSON.stringify({ path: '.env', contents: 'TOKEN=x' }),
|
|
132
|
+
latestUserPrompt: prompt,
|
|
133
|
+
});
|
|
134
|
+
expect(second.decision).toBe('approved_once');
|
|
135
|
+
|
|
136
|
+
const third = runtime.evaluateToolCall({
|
|
137
|
+
toolName: 'write',
|
|
138
|
+
argsJson: JSON.stringify({ path: '.env', contents: 'TOKEN=x' }),
|
|
139
|
+
latestUserPrompt: prompt,
|
|
140
|
+
});
|
|
141
|
+
expect(third.decision).toBe('required');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('yes for agent persists trust across runtime restarts', () => {
|
|
145
|
+
const trustStorePath = tempTrustStorePath('agent-trust');
|
|
146
|
+
const policyPath = '/tmp/hybridclaw-missing-policy.yaml';
|
|
147
|
+
const prompt = 'Fetch from example.com';
|
|
148
|
+
const argsJson = JSON.stringify({ url: 'https://example.com' });
|
|
149
|
+
|
|
150
|
+
const runtime = new TrustedCoworkerApprovalRuntime(
|
|
151
|
+
policyPath,
|
|
152
|
+
trustStorePath,
|
|
153
|
+
);
|
|
154
|
+
const first = runtime.evaluateToolCall({
|
|
155
|
+
toolName: 'web_fetch',
|
|
156
|
+
argsJson,
|
|
157
|
+
latestUserPrompt: prompt,
|
|
158
|
+
});
|
|
159
|
+
expect(first.decision).toBe('required');
|
|
160
|
+
|
|
161
|
+
const prelude = runtime.handleApprovalResponse([
|
|
162
|
+
userMessage('yes for agent'),
|
|
163
|
+
]);
|
|
164
|
+
expect(prelude?.approvalMode).toBe('agent');
|
|
165
|
+
|
|
166
|
+
const second = runtime.evaluateToolCall({
|
|
167
|
+
toolName: 'web_fetch',
|
|
168
|
+
argsJson,
|
|
169
|
+
latestUserPrompt: prompt,
|
|
170
|
+
});
|
|
171
|
+
expect(second.decision).toBe('approved_agent');
|
|
172
|
+
|
|
173
|
+
const restarted = new TrustedCoworkerApprovalRuntime(
|
|
174
|
+
policyPath,
|
|
175
|
+
trustStorePath,
|
|
176
|
+
);
|
|
177
|
+
const third = restarted.evaluateToolCall({
|
|
178
|
+
toolName: 'web_fetch',
|
|
179
|
+
argsJson,
|
|
180
|
+
latestUserPrompt: prompt,
|
|
181
|
+
});
|
|
182
|
+
expect(third.decision).toBe('approved_agent');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('pinned red cannot be agent-trusted across restarts', () => {
|
|
186
|
+
const trustStorePath = tempTrustStorePath('pinned-agent');
|
|
187
|
+
const policyPath = '/tmp/hybridclaw-missing-policy.yaml';
|
|
188
|
+
const prompt = 'Write .env';
|
|
189
|
+
const argsJson = JSON.stringify({ path: '.env', contents: 'TOKEN=abc' });
|
|
190
|
+
|
|
191
|
+
const runtime = new TrustedCoworkerApprovalRuntime(
|
|
192
|
+
policyPath,
|
|
193
|
+
trustStorePath,
|
|
194
|
+
);
|
|
195
|
+
const first = runtime.evaluateToolCall({
|
|
196
|
+
toolName: 'write',
|
|
197
|
+
argsJson,
|
|
198
|
+
latestUserPrompt: prompt,
|
|
199
|
+
});
|
|
200
|
+
expect(first.decision).toBe('required');
|
|
201
|
+
expect(first.pinned).toBe(true);
|
|
202
|
+
|
|
203
|
+
const prelude = runtime.handleApprovalResponse([userMessage('3')]);
|
|
204
|
+
expect(prelude?.approvalMode).toBe('once');
|
|
205
|
+
|
|
206
|
+
const second = runtime.evaluateToolCall({
|
|
207
|
+
toolName: 'write',
|
|
208
|
+
argsJson,
|
|
209
|
+
latestUserPrompt: prompt,
|
|
210
|
+
});
|
|
211
|
+
expect(second.decision).toBe('approved_once');
|
|
212
|
+
|
|
213
|
+
const restarted = new TrustedCoworkerApprovalRuntime(
|
|
214
|
+
policyPath,
|
|
215
|
+
trustStorePath,
|
|
216
|
+
);
|
|
217
|
+
const third = restarted.evaluateToolCall({
|
|
218
|
+
toolName: 'write',
|
|
219
|
+
argsJson,
|
|
220
|
+
latestUserPrompt: prompt,
|
|
221
|
+
});
|
|
222
|
+
expect(third.decision).toBe('required');
|
|
223
|
+
});
|
|
224
|
+
});
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { expect, test } from 'vitest';
|
|
2
2
|
|
|
3
3
|
import { buildResponseText } from '../src/channels/discord/delivery.js';
|
|
4
|
-
import {
|
|
4
|
+
import { isTrigger, parseCommand } from '../src/channels/discord/inbound.js';
|
|
5
|
+
import {
|
|
6
|
+
type MentionLookup,
|
|
7
|
+
rewriteUserMentions,
|
|
8
|
+
} from '../src/channels/discord/mentions.js';
|
|
5
9
|
|
|
6
10
|
function createLookup(entries: Record<string, string[]>): MentionLookup {
|
|
7
11
|
const byAlias = new Map<string, Set<string>>();
|
|
@@ -18,7 +22,9 @@ test('rewriteUserMentions rewrites a uniquely-resolved @alias', () => {
|
|
|
18
22
|
});
|
|
19
23
|
|
|
20
24
|
test('rewriteUserMentions does not rewrite ambiguous aliases', () => {
|
|
21
|
-
const lookup = createLookup({
|
|
25
|
+
const lookup = createLookup({
|
|
26
|
+
bob: ['111111111111111111', '222222222222222222'],
|
|
27
|
+
});
|
|
22
28
|
const output = rewriteUserMentions('hi @bob', lookup);
|
|
23
29
|
expect(output).toBe('hi @bob');
|
|
24
30
|
});
|
|
@@ -41,3 +47,77 @@ test('buildResponseText leaves text unchanged when no tools were used', () => {
|
|
|
41
47
|
const output = buildResponseText('Done.');
|
|
42
48
|
expect(output).toBe('Done.');
|
|
43
49
|
});
|
|
50
|
+
|
|
51
|
+
test('isTrigger blocks non-command chatter when channel mode is off', () => {
|
|
52
|
+
const shouldTrigger = isTrigger({
|
|
53
|
+
content: 'hello',
|
|
54
|
+
isDm: false,
|
|
55
|
+
commandsOnly: false,
|
|
56
|
+
respondToAllMessages: false,
|
|
57
|
+
guildMessageMode: 'off',
|
|
58
|
+
prefix: '!claw',
|
|
59
|
+
botMentionRegex: null,
|
|
60
|
+
hasBotMention: false,
|
|
61
|
+
});
|
|
62
|
+
expect(shouldTrigger).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('isTrigger still allows prefixed commands when channel mode is off', () => {
|
|
66
|
+
const shouldTrigger = isTrigger({
|
|
67
|
+
content: '!claw status',
|
|
68
|
+
isDm: false,
|
|
69
|
+
commandsOnly: false,
|
|
70
|
+
respondToAllMessages: false,
|
|
71
|
+
guildMessageMode: 'off',
|
|
72
|
+
prefix: '!claw',
|
|
73
|
+
botMentionRegex: null,
|
|
74
|
+
hasBotMention: false,
|
|
75
|
+
});
|
|
76
|
+
expect(shouldTrigger).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('isTrigger allows free-response mode in guild channels', () => {
|
|
80
|
+
const shouldTrigger = isTrigger({
|
|
81
|
+
content: 'Can you review this patch?',
|
|
82
|
+
isDm: false,
|
|
83
|
+
commandsOnly: false,
|
|
84
|
+
respondToAllMessages: false,
|
|
85
|
+
guildMessageMode: 'free',
|
|
86
|
+
prefix: '!claw',
|
|
87
|
+
botMentionRegex: null,
|
|
88
|
+
hasBotMention: false,
|
|
89
|
+
});
|
|
90
|
+
expect(shouldTrigger).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('isTrigger keeps mention mode even when respondToAllMessages is enabled', () => {
|
|
94
|
+
const shouldTrigger = isTrigger({
|
|
95
|
+
content: 'hello',
|
|
96
|
+
isDm: false,
|
|
97
|
+
commandsOnly: false,
|
|
98
|
+
respondToAllMessages: true,
|
|
99
|
+
guildMessageMode: 'mention',
|
|
100
|
+
prefix: '!claw',
|
|
101
|
+
botMentionRegex: null,
|
|
102
|
+
hasBotMention: false,
|
|
103
|
+
});
|
|
104
|
+
expect(shouldTrigger).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('parseCommand recognizes channel command namespace', () => {
|
|
108
|
+
const parsed = parseCommand('!claw channel mode free', null, '!claw');
|
|
109
|
+
expect(parsed).toEqual({
|
|
110
|
+
isCommand: true,
|
|
111
|
+
command: 'channel',
|
|
112
|
+
args: ['mode', 'free'],
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('parseCommand recognizes usage command namespace', () => {
|
|
117
|
+
const parsed = parseCommand('!claw usage monthly', null, '!claw');
|
|
118
|
+
expect(parsed).toEqual({
|
|
119
|
+
isCommand: true,
|
|
120
|
+
command: 'usage',
|
|
121
|
+
args: ['monthly'],
|
|
122
|
+
});
|
|
123
|
+
});
|