@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/skills.ts
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
* The system prompt includes skill metadata + location, and inlines full
|
|
4
4
|
* bodies for skills marked `always: true`.
|
|
5
5
|
*/
|
|
6
|
+
|
|
7
|
+
import { createHash } from 'crypto';
|
|
6
8
|
import fs from 'fs';
|
|
7
9
|
import os from 'os';
|
|
8
10
|
import path from 'path';
|
|
9
|
-
import { createHash } from 'crypto';
|
|
10
11
|
import { fileURLToPath } from 'url';
|
|
11
12
|
|
|
12
13
|
import { agentWorkspaceDir } from './ipc.js';
|
|
@@ -205,7 +206,9 @@ function parseInlineStringList(raw: string): string[] {
|
|
|
205
206
|
function normalizeStringList(raw: unknown): string[] {
|
|
206
207
|
if (Array.isArray(raw)) {
|
|
207
208
|
return raw
|
|
208
|
-
.map((item) =>
|
|
209
|
+
.map((item) =>
|
|
210
|
+
typeof item === 'string' ? item.trim() : String(item ?? '').trim(),
|
|
211
|
+
)
|
|
209
212
|
.filter(Boolean);
|
|
210
213
|
}
|
|
211
214
|
if (typeof raw === 'string') {
|
|
@@ -221,7 +224,8 @@ function normalizeStringList(raw: unknown): string[] {
|
|
|
221
224
|
|
|
222
225
|
function tryParseJsonObject(raw: string): Record<string, unknown> | null {
|
|
223
226
|
const trimmed = stripQuotes(raw.trim());
|
|
224
|
-
if (!trimmed || (!trimmed.startsWith('{') && !trimmed.startsWith('[')))
|
|
227
|
+
if (!trimmed || (!trimmed.startsWith('{') && !trimmed.startsWith('[')))
|
|
228
|
+
return null;
|
|
225
229
|
try {
|
|
226
230
|
const parsed = JSON.parse(trimmed) as unknown;
|
|
227
231
|
if (isRecord(parsed)) return parsed;
|
|
@@ -231,7 +235,10 @@ function tryParseJsonObject(raw: string): Record<string, unknown> | null {
|
|
|
231
235
|
return null;
|
|
232
236
|
}
|
|
233
237
|
|
|
234
|
-
function extractTopLevelSection(
|
|
238
|
+
function extractTopLevelSection(
|
|
239
|
+
block: string,
|
|
240
|
+
key: string,
|
|
241
|
+
): FrontmatterSection | null {
|
|
235
242
|
const lines = block.split('\n');
|
|
236
243
|
for (let i = 0; i < lines.length; i += 1) {
|
|
237
244
|
const line = lines[i] || '';
|
|
@@ -264,9 +271,11 @@ function extractTopLevelSection(block: string, key: string): FrontmatterSection
|
|
|
264
271
|
return null;
|
|
265
272
|
}
|
|
266
273
|
|
|
267
|
-
function parseSectionChildren(
|
|
274
|
+
function parseSectionChildren(
|
|
275
|
+
children: string[],
|
|
276
|
+
): Map<string, FrontmatterSection> {
|
|
268
277
|
const parsed = new Map<string, FrontmatterSection>();
|
|
269
|
-
for (let i = 0; i < children.length;) {
|
|
278
|
+
for (let i = 0; i < children.length; ) {
|
|
270
279
|
const line = children[i] || '';
|
|
271
280
|
const trimmed = line.trim();
|
|
272
281
|
if (!trimmed) {
|
|
@@ -305,7 +314,9 @@ function parseSectionChildren(children: string[]): Map<string, FrontmatterSectio
|
|
|
305
314
|
return parsed;
|
|
306
315
|
}
|
|
307
316
|
|
|
308
|
-
function parseSectionStringList(
|
|
317
|
+
function parseSectionStringList(
|
|
318
|
+
section: FrontmatterSection | undefined,
|
|
319
|
+
): string[] {
|
|
309
320
|
if (!section) return [];
|
|
310
321
|
const inline = parseInlineStringList(section.inline);
|
|
311
322
|
if (inline.length > 0 || section.inline.trim() === '[]') return inline;
|
|
@@ -324,7 +335,9 @@ function parseRequiresFromFrontmatter(frontmatter: FrontmatterParseResult): {
|
|
|
324
335
|
bins: string[];
|
|
325
336
|
env: string[];
|
|
326
337
|
} {
|
|
327
|
-
const fromInlineJson = frontmatter.meta.requires
|
|
338
|
+
const fromInlineJson = frontmatter.meta.requires
|
|
339
|
+
? tryParseJsonObject(frontmatter.meta.requires)
|
|
340
|
+
: null;
|
|
328
341
|
if (fromInlineJson) {
|
|
329
342
|
return {
|
|
330
343
|
bins: normalizeStringList(fromInlineJson.bins),
|
|
@@ -354,15 +367,21 @@ function parseHybridClawMetadata(frontmatter: FrontmatterParseResult): {
|
|
|
354
367
|
tags: string[];
|
|
355
368
|
relatedSkills: string[];
|
|
356
369
|
} {
|
|
357
|
-
const normalizeMetadata = (
|
|
370
|
+
const normalizeMetadata = (
|
|
371
|
+
raw: Record<string, unknown>,
|
|
372
|
+
): { tags: string[]; relatedSkills: string[] } => {
|
|
358
373
|
const hybridRaw = isRecord(raw.hybridclaw) ? raw.hybridclaw : raw;
|
|
359
374
|
return {
|
|
360
375
|
tags: normalizeStringList(hybridRaw.tags),
|
|
361
|
-
relatedSkills: normalizeStringList(
|
|
376
|
+
relatedSkills: normalizeStringList(
|
|
377
|
+
hybridRaw.related_skills ?? hybridRaw.relatedSkills,
|
|
378
|
+
),
|
|
362
379
|
};
|
|
363
380
|
};
|
|
364
381
|
|
|
365
|
-
const fromInlineJson = frontmatter.meta.metadata
|
|
382
|
+
const fromInlineJson = frontmatter.meta.metadata
|
|
383
|
+
? tryParseJsonObject(frontmatter.meta.metadata)
|
|
384
|
+
: null;
|
|
366
385
|
if (fromInlineJson) return normalizeMetadata(fromInlineJson);
|
|
367
386
|
|
|
368
387
|
const metadataSection = extractTopLevelSection(frontmatter.block, 'metadata');
|
|
@@ -394,7 +413,8 @@ function hasBinary(binName: string): boolean {
|
|
|
394
413
|
if (!bin) return false;
|
|
395
414
|
|
|
396
415
|
const currentPath = process.env.PATH || '';
|
|
397
|
-
const currentPathExt =
|
|
416
|
+
const currentPathExt =
|
|
417
|
+
process.platform === 'win32' ? process.env.PATHEXT || '' : '';
|
|
398
418
|
if (cachedPathEnv !== currentPath || cachedPathExt !== currentPathExt) {
|
|
399
419
|
cachedPathEnv = currentPath;
|
|
400
420
|
cachedPathExt = currentPathExt;
|
|
@@ -404,9 +424,16 @@ function hasBinary(binName: string): boolean {
|
|
|
404
424
|
const cached = hasBinaryCache.get(bin);
|
|
405
425
|
if (cached != null) return cached;
|
|
406
426
|
|
|
407
|
-
const exts =
|
|
408
|
-
|
|
409
|
-
|
|
427
|
+
const exts =
|
|
428
|
+
process.platform === 'win32'
|
|
429
|
+
? [
|
|
430
|
+
'',
|
|
431
|
+
...currentPathExt
|
|
432
|
+
.split(';')
|
|
433
|
+
.map((ext) => ext.trim())
|
|
434
|
+
.filter(Boolean),
|
|
435
|
+
]
|
|
436
|
+
: [''];
|
|
410
437
|
for (const part of currentPath.split(path.delimiter).filter(Boolean)) {
|
|
411
438
|
for (const ext of exts) {
|
|
412
439
|
const candidate = path.join(part, `${bin}${ext}`);
|
|
@@ -448,7 +475,10 @@ function pathWithin(root: string, target: string): boolean {
|
|
|
448
475
|
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
449
476
|
}
|
|
450
477
|
|
|
451
|
-
function asContainerPath(
|
|
478
|
+
function asContainerPath(
|
|
479
|
+
workspaceDir: string,
|
|
480
|
+
absolutePath: string,
|
|
481
|
+
): string | null {
|
|
452
482
|
if (!pathWithin(workspaceDir, absolutePath)) return null;
|
|
453
483
|
const rel = toPosixPath(path.relative(workspaceDir, absolutePath));
|
|
454
484
|
return rel ? `/workspace/${rel}` : '/workspace';
|
|
@@ -515,7 +545,10 @@ function scanSkillsDir(dir: string, source: SkillSource): SkillCandidate[] {
|
|
|
515
545
|
name,
|
|
516
546
|
description: (meta.description || '').trim(),
|
|
517
547
|
userInvocable: parseBool(meta['user-invocable'], true),
|
|
518
|
-
disableModelInvocation: parseBool(
|
|
548
|
+
disableModelInvocation: parseBool(
|
|
549
|
+
meta['disable-model-invocation'],
|
|
550
|
+
false,
|
|
551
|
+
),
|
|
519
552
|
always,
|
|
520
553
|
requires,
|
|
521
554
|
metadata: {
|
|
@@ -599,8 +632,14 @@ function resolveSyncedSkillTarget(
|
|
|
599
632
|
};
|
|
600
633
|
}
|
|
601
634
|
|
|
602
|
-
function syncSkillIntoWorkspace(
|
|
603
|
-
|
|
635
|
+
function syncSkillIntoWorkspace(
|
|
636
|
+
skill: SkillCandidate,
|
|
637
|
+
workspaceDir: string,
|
|
638
|
+
): string {
|
|
639
|
+
const { rootDir, targetDir, targetSkillFile } = resolveSyncedSkillTarget(
|
|
640
|
+
skill,
|
|
641
|
+
workspaceDir,
|
|
642
|
+
);
|
|
604
643
|
fs.mkdirSync(rootDir, { recursive: true });
|
|
605
644
|
|
|
606
645
|
if (!pathWithin(rootDir, targetDir)) {
|
|
@@ -641,8 +680,14 @@ function sanitizeCommandName(name: string): string {
|
|
|
641
680
|
.slice(0, MAX_SKILL_COMMAND_NAME_LENGTH);
|
|
642
681
|
}
|
|
643
682
|
|
|
644
|
-
function resolveUniqueCommandName(
|
|
645
|
-
|
|
683
|
+
function resolveUniqueCommandName(
|
|
684
|
+
baseName: string,
|
|
685
|
+
usedNames: Set<string>,
|
|
686
|
+
): string | null {
|
|
687
|
+
const normalizedBase = (baseName || 'skill').slice(
|
|
688
|
+
0,
|
|
689
|
+
MAX_SKILL_COMMAND_NAME_LENGTH,
|
|
690
|
+
);
|
|
646
691
|
if (!usedNames.has(normalizedBase)) {
|
|
647
692
|
usedNames.add(normalizedBase);
|
|
648
693
|
return normalizedBase;
|
|
@@ -650,7 +695,10 @@ function resolveUniqueCommandName(baseName: string, usedNames: Set<string>): str
|
|
|
650
695
|
|
|
651
696
|
for (let index = 2; index < 10_000; index += 1) {
|
|
652
697
|
const suffix = `-${index}`;
|
|
653
|
-
const prefixLen = Math.max(
|
|
698
|
+
const prefixLen = Math.max(
|
|
699
|
+
1,
|
|
700
|
+
MAX_SKILL_COMMAND_NAME_LENGTH - suffix.length,
|
|
701
|
+
);
|
|
654
702
|
const candidate = `${normalizedBase.slice(0, prefixLen)}${suffix}`;
|
|
655
703
|
if (usedNames.has(candidate)) continue;
|
|
656
704
|
usedNames.add(candidate);
|
|
@@ -660,7 +708,9 @@ function resolveUniqueCommandName(baseName: string, usedNames: Set<string>): str
|
|
|
660
708
|
}
|
|
661
709
|
|
|
662
710
|
function buildSkillCommandSpecs(skills: Skill[]): SkillCommandSpec[] {
|
|
663
|
-
const used = new Set<string>(
|
|
711
|
+
const used = new Set<string>(
|
|
712
|
+
Array.from(RESERVED_SKILL_COMMAND_NAMES.values()),
|
|
713
|
+
);
|
|
664
714
|
const specs: SkillCommandSpec[] = [];
|
|
665
715
|
|
|
666
716
|
for (const skill of skills) {
|
|
@@ -678,29 +728,39 @@ function buildSkillCommandSpecs(skills: Skill[]): SkillCommandSpec[] {
|
|
|
678
728
|
return specs;
|
|
679
729
|
}
|
|
680
730
|
|
|
681
|
-
function findSkillCommand(
|
|
731
|
+
function findSkillCommand(
|
|
732
|
+
skillCommands: SkillCommandSpec[],
|
|
733
|
+
rawName: string,
|
|
734
|
+
): SkillCommandSpec | null {
|
|
682
735
|
const lowered = rawName.trim().toLowerCase();
|
|
683
736
|
if (!lowered) return null;
|
|
684
737
|
const sanitized = sanitizeCommandName(rawName);
|
|
685
|
-
return
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
738
|
+
return (
|
|
739
|
+
skillCommands.find(
|
|
740
|
+
(entry) =>
|
|
741
|
+
entry.name === lowered || (sanitized && entry.name === sanitized),
|
|
742
|
+
) || null
|
|
743
|
+
);
|
|
689
744
|
}
|
|
690
745
|
|
|
691
746
|
function findInvocableSkill(skills: Skill[], rawName: string): Skill | null {
|
|
692
747
|
const target = rawName.trim().toLowerCase();
|
|
693
748
|
if (!target) return null;
|
|
694
749
|
const normalizedTarget = normalizeSkillLookup(rawName);
|
|
695
|
-
return
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
750
|
+
return (
|
|
751
|
+
skills.find((skill) => {
|
|
752
|
+
if (!skill.userInvocable) return false;
|
|
753
|
+
const name = skill.name.toLowerCase();
|
|
754
|
+
if (name === target) return true;
|
|
755
|
+
return normalizeSkillLookup(skill.name) === normalizedTarget;
|
|
756
|
+
}) || null
|
|
757
|
+
);
|
|
701
758
|
}
|
|
702
759
|
|
|
703
|
-
function parseSkillInvocation(
|
|
760
|
+
function parseSkillInvocation(
|
|
761
|
+
content: string,
|
|
762
|
+
skills: Skill[],
|
|
763
|
+
): { skill: Skill; args: string } | null {
|
|
704
764
|
const trimmed = content.trim();
|
|
705
765
|
if (!trimmed.startsWith('/')) return null;
|
|
706
766
|
const skillCommands = buildSkillCommandSpecs(skills);
|
|
@@ -719,7 +779,10 @@ function parseSkillInvocation(content: string, skills: Skill[]): { skill: Skill;
|
|
|
719
779
|
if (!skillMatch) return null;
|
|
720
780
|
const explicitName = (skillMatch[1] || '').trim();
|
|
721
781
|
const explicitSkill = findInvocableSkill(skills, explicitName);
|
|
722
|
-
const skill =
|
|
782
|
+
const skill =
|
|
783
|
+
explicitSkill ||
|
|
784
|
+
findSkillCommand(skillCommands, explicitName)?.skill ||
|
|
785
|
+
null;
|
|
723
786
|
if (!skill) return null;
|
|
724
787
|
return { skill, args: (skillMatch[2] || '').trim() };
|
|
725
788
|
}
|
|
@@ -728,7 +791,10 @@ function parseSkillInvocation(content: string, skills: Skill[]): { skill: Skill;
|
|
|
728
791
|
const skillName = commandName.slice('skill:'.length).trim();
|
|
729
792
|
if (!skillName) return null;
|
|
730
793
|
const explicitSkill = findInvocableSkill(skills, skillName);
|
|
731
|
-
const skill =
|
|
794
|
+
const skill =
|
|
795
|
+
explicitSkill ||
|
|
796
|
+
findSkillCommand(skillCommands, skillName)?.skill ||
|
|
797
|
+
null;
|
|
732
798
|
if (!skill) return null;
|
|
733
799
|
return { skill, args: remainder };
|
|
734
800
|
}
|
|
@@ -745,7 +811,10 @@ function loadSkillBody(skill: Skill, maxChars: number): string {
|
|
|
745
811
|
if (body.length <= maxChars) return body;
|
|
746
812
|
return `${body.slice(0, maxChars)}\n\n[truncated]`;
|
|
747
813
|
} catch (err) {
|
|
748
|
-
logger.warn(
|
|
814
|
+
logger.warn(
|
|
815
|
+
{ skill: skill.name, path: skill.filePath, err },
|
|
816
|
+
'Failed to load SKILL.md body',
|
|
817
|
+
);
|
|
749
818
|
return '';
|
|
750
819
|
}
|
|
751
820
|
}
|
|
@@ -757,7 +826,10 @@ function loadSkillBody(skill: Skill, maxChars: number): string {
|
|
|
757
826
|
* - /skill:<name> [input]
|
|
758
827
|
* - /<name> [input] (user-invocable skills)
|
|
759
828
|
*/
|
|
760
|
-
export function expandSkillInvocation(
|
|
829
|
+
export function expandSkillInvocation(
|
|
830
|
+
content: string,
|
|
831
|
+
skills: Skill[],
|
|
832
|
+
): string {
|
|
761
833
|
const invocation = parseSkillInvocation(content, skills);
|
|
762
834
|
if (!invocation) return content;
|
|
763
835
|
|
|
@@ -771,12 +843,7 @@ export function expandSkillInvocation(content: string, skills: Skill[]): string
|
|
|
771
843
|
];
|
|
772
844
|
|
|
773
845
|
if (body) {
|
|
774
|
-
lines.push(
|
|
775
|
-
'',
|
|
776
|
-
'<skill_instructions>',
|
|
777
|
-
body,
|
|
778
|
-
'</skill_instructions>',
|
|
779
|
-
);
|
|
846
|
+
lines.push('', '<skill_instructions>', body, '</skill_instructions>');
|
|
780
847
|
} else {
|
|
781
848
|
lines.push('Read the skill file with the `read` tool and follow it.');
|
|
782
849
|
}
|
|
@@ -804,11 +871,19 @@ export function loadSkills(agentId: string): Skill[] {
|
|
|
804
871
|
const agentsPersonalSkillsDir = path.join(os.homedir(), '.agents', 'skills');
|
|
805
872
|
|
|
806
873
|
const extraSkills = extraDirs.flatMap((dir) => scanSkillsDir(dir, 'extra'));
|
|
807
|
-
const bundledSkills = bundledSkillsDir
|
|
874
|
+
const bundledSkills = bundledSkillsDir
|
|
875
|
+
? scanSkillsDir(bundledSkillsDir, 'bundled')
|
|
876
|
+
: [];
|
|
808
877
|
const codexSkills = codexDirs.flatMap((dir) => scanSkillsDir(dir, 'codex'));
|
|
809
878
|
const claudeSkills = scanSkillsDir(claudeSkillsDir, 'claude');
|
|
810
|
-
const agentsPersonalSkills = scanSkillsDir(
|
|
811
|
-
|
|
879
|
+
const agentsPersonalSkills = scanSkillsDir(
|
|
880
|
+
agentsPersonalSkillsDir,
|
|
881
|
+
'agents-personal',
|
|
882
|
+
);
|
|
883
|
+
const projectAgentsSkills = scanSkillsDir(
|
|
884
|
+
PROJECT_AGENTS_SKILLS_DIR,
|
|
885
|
+
'agents-project',
|
|
886
|
+
);
|
|
812
887
|
const workspaceSkills = scanSkillsDir(WORKSPACE_SKILLS_DIR, 'workspace');
|
|
813
888
|
|
|
814
889
|
const byName = new Map<string, SkillCandidate>();
|
|
@@ -822,8 +897,9 @@ export function loadSkills(agentId: string): Skill[] {
|
|
|
822
897
|
for (const skill of projectAgentsSkills) byName.set(skill.name, skill);
|
|
823
898
|
for (const skill of workspaceSkills) byName.set(skill.name, skill);
|
|
824
899
|
|
|
825
|
-
const eligible = Array.from(byName.values())
|
|
826
|
-
|
|
900
|
+
const eligible = Array.from(byName.values()).filter(
|
|
901
|
+
(skill) => checkEligibility(skill).available,
|
|
902
|
+
);
|
|
827
903
|
const guarded = eligible.filter((skill) => {
|
|
828
904
|
const decision = guardSkillDirectory({
|
|
829
905
|
skillName: skill.name,
|
|
@@ -835,14 +911,17 @@ export function loadSkills(agentId: string): Skill[] {
|
|
|
835
911
|
const fingerprint = `${path.resolve(skill.baseDir)}:${decision.result.verdict}:${decision.result.findings.length}`;
|
|
836
912
|
if (!warnedBlockedSkills.has(fingerprint)) {
|
|
837
913
|
warnedBlockedSkills.add(fingerprint);
|
|
838
|
-
logger.warn(
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
914
|
+
logger.warn(
|
|
915
|
+
{
|
|
916
|
+
skill: skill.name,
|
|
917
|
+
source: skill.source,
|
|
918
|
+
trustLevel: decision.result.trustLevel,
|
|
919
|
+
verdict: decision.result.verdict,
|
|
920
|
+
findings: decision.result.findings.length,
|
|
921
|
+
reason: decision.reason,
|
|
922
|
+
},
|
|
923
|
+
'Blocked skill by security scanner',
|
|
924
|
+
);
|
|
846
925
|
}
|
|
847
926
|
return false;
|
|
848
927
|
});
|
|
@@ -850,13 +929,22 @@ export function loadSkills(agentId: string): Skill[] {
|
|
|
850
929
|
const resolved: Skill[] = [];
|
|
851
930
|
for (const skill of guarded) {
|
|
852
931
|
try {
|
|
853
|
-
let containerSkillPath = asContainerPath(
|
|
932
|
+
let containerSkillPath = asContainerPath(
|
|
933
|
+
workspaceDir,
|
|
934
|
+
path.resolve(skill.filePath),
|
|
935
|
+
);
|
|
854
936
|
if (!containerSkillPath) {
|
|
855
937
|
const syncedSkillFile = syncSkillIntoWorkspace(skill, workspaceDir);
|
|
856
|
-
containerSkillPath = asContainerPath(
|
|
938
|
+
containerSkillPath = asContainerPath(
|
|
939
|
+
workspaceDir,
|
|
940
|
+
path.resolve(syncedSkillFile),
|
|
941
|
+
);
|
|
857
942
|
}
|
|
858
943
|
if (!containerSkillPath) {
|
|
859
|
-
logger.warn(
|
|
944
|
+
logger.warn(
|
|
945
|
+
{ skill: skill.name, path: skill.filePath },
|
|
946
|
+
'Could not resolve container-readable skill path',
|
|
947
|
+
);
|
|
860
948
|
continue;
|
|
861
949
|
}
|
|
862
950
|
|
|
@@ -865,7 +953,10 @@ export function loadSkills(agentId: string): Skill[] {
|
|
|
865
953
|
location: containerSkillPath,
|
|
866
954
|
});
|
|
867
955
|
} catch (err) {
|
|
868
|
-
logger.warn(
|
|
956
|
+
logger.warn(
|
|
957
|
+
{ skill: skill.name, err },
|
|
958
|
+
'Failed to resolve skill location',
|
|
959
|
+
);
|
|
869
960
|
}
|
|
870
961
|
}
|
|
871
962
|
|
|
@@ -886,7 +977,9 @@ export function buildSkillsPrompt(skills: Skill[]): string {
|
|
|
886
977
|
const demotedAlways: Skill[] = [];
|
|
887
978
|
|
|
888
979
|
let alwaysChars = 0;
|
|
889
|
-
for (const skill of promptCandidates.filter(
|
|
980
|
+
for (const skill of promptCandidates.filter(
|
|
981
|
+
(candidate) => candidate.always,
|
|
982
|
+
)) {
|
|
890
983
|
const body = loadSkillBody(skill, Number.MAX_SAFE_INTEGER);
|
|
891
984
|
if (!body) {
|
|
892
985
|
demotedAlways.push(skill);
|
|
@@ -909,10 +1002,15 @@ export function buildSkillsPrompt(skills: Skill[]): string {
|
|
|
909
1002
|
|
|
910
1003
|
if (demotedAlways.length > 0) {
|
|
911
1004
|
const demotedNames = demotedAlways.map((skill) => skill.name).join(', ');
|
|
912
|
-
lines.push(
|
|
1005
|
+
lines.push(
|
|
1006
|
+
`⚠️ maxAlwaysChars=${MAX_ALWAYS_CHARS} exceeded; demoted to summary: ${demotedNames}`,
|
|
1007
|
+
'',
|
|
1008
|
+
);
|
|
913
1009
|
}
|
|
914
1010
|
|
|
915
|
-
const summaryCandidates = promptCandidates.filter(
|
|
1011
|
+
const summaryCandidates = promptCandidates.filter(
|
|
1012
|
+
(skill) => !embeddedAlways.has(skill.name),
|
|
1013
|
+
);
|
|
916
1014
|
if (summaryCandidates.length > 0) {
|
|
917
1015
|
lines.push('<available_skills>');
|
|
918
1016
|
|
package/src/token-efficiency.ts
CHANGED
|
@@ -67,13 +67,17 @@ function trimToRecentWithinBudget(
|
|
|
67
67
|
return kept.reverse();
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
export function estimateTokenCountFromText(
|
|
70
|
+
export function estimateTokenCountFromText(
|
|
71
|
+
text: string | null | undefined,
|
|
72
|
+
): number {
|
|
71
73
|
const normalized = typeof text === 'string' ? text : '';
|
|
72
74
|
if (!normalized) return 0;
|
|
73
75
|
return Math.max(1, Math.ceil(normalized.length / DEFAULT_CHARS_PER_TOKEN));
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
function normalizeMessageContentToText(
|
|
78
|
+
function normalizeMessageContentToText(
|
|
79
|
+
content: ChatMessage['content'],
|
|
80
|
+
): string {
|
|
77
81
|
if (typeof content === 'string') return content;
|
|
78
82
|
if (!Array.isArray(content)) return '';
|
|
79
83
|
const chunks: string[] = [];
|
|
@@ -98,16 +102,24 @@ export function estimateTokenCountFromMessages(
|
|
|
98
102
|
for (const message of messages) {
|
|
99
103
|
total += 4; // Approximate per-message framing overhead.
|
|
100
104
|
total += estimateTokenCountFromText(message.role);
|
|
101
|
-
total += estimateTokenCountFromText(
|
|
105
|
+
total += estimateTokenCountFromText(
|
|
106
|
+
normalizeMessageContentToText(message.content),
|
|
107
|
+
);
|
|
102
108
|
}
|
|
103
109
|
return total;
|
|
104
110
|
}
|
|
105
111
|
|
|
106
|
-
export function truncateMessageContent(
|
|
112
|
+
export function truncateMessageContent(
|
|
113
|
+
content: string,
|
|
114
|
+
maxChars: number,
|
|
115
|
+
): string {
|
|
107
116
|
if (!Number.isFinite(maxChars) || maxChars <= 0) return '';
|
|
108
117
|
if (content.length <= maxChars) return content;
|
|
109
118
|
|
|
110
|
-
const bodyMax = Math.max(
|
|
119
|
+
const bodyMax = Math.max(
|
|
120
|
+
0,
|
|
121
|
+
Math.floor(maxChars) - MESSAGE_TRUNCATED_MARKER.length,
|
|
122
|
+
);
|
|
111
123
|
if (bodyMax <= 0) {
|
|
112
124
|
return content.slice(0, Math.floor(maxChars));
|
|
113
125
|
}
|
|
@@ -164,15 +176,22 @@ export function optimizeHistoryMessagesForPrompt(
|
|
|
164
176
|
);
|
|
165
177
|
const protectHeadMessages = Math.max(
|
|
166
178
|
0,
|
|
167
|
-
Math.floor(
|
|
179
|
+
Math.floor(
|
|
180
|
+
options?.protectHeadMessages ?? DEFAULT_HISTORY_PROTECT_HEAD_MESSAGES,
|
|
181
|
+
),
|
|
168
182
|
);
|
|
169
183
|
const protectTailMessages = Math.max(
|
|
170
184
|
0,
|
|
171
|
-
Math.floor(
|
|
185
|
+
Math.floor(
|
|
186
|
+
options?.protectTailMessages ?? DEFAULT_HISTORY_PROTECT_TAIL_MESSAGES,
|
|
187
|
+
),
|
|
172
188
|
);
|
|
173
189
|
|
|
174
190
|
const originalCount = messages.length;
|
|
175
|
-
const originalChars = messages.reduce(
|
|
191
|
+
const originalChars = messages.reduce(
|
|
192
|
+
(total, message) => total + message.content.length,
|
|
193
|
+
0,
|
|
194
|
+
);
|
|
176
195
|
let perMessageTruncatedCount = 0;
|
|
177
196
|
|
|
178
197
|
const normalized = messages.map((message) => {
|
|
@@ -191,7 +210,10 @@ export function optimizeHistoryMessagesForPrompt(
|
|
|
191
210
|
if (preBudgetChars > maxTotalChars) {
|
|
192
211
|
middleCompressionApplied = true;
|
|
193
212
|
const headCount = Math.min(protectHeadMessages, normalized.length);
|
|
194
|
-
const tailCount = Math.min(
|
|
213
|
+
const tailCount = Math.min(
|
|
214
|
+
protectTailMessages,
|
|
215
|
+
Math.max(0, normalized.length - headCount),
|
|
216
|
+
);
|
|
195
217
|
const middleStart = headCount;
|
|
196
218
|
const middleEnd = normalized.length - tailCount;
|
|
197
219
|
const head = normalized.slice(0, headCount);
|