Skip to content

Commit 2777e52

Browse files
authored
Merge pull request #4 from AelfScanProject/codex/skill-unified-upgrade-20260227
Codex/skill unified upgrade 20260227
2 parents b8b6d43 + 817de73 commit 2777e52

7 files changed

Lines changed: 285 additions & 170 deletions

File tree

.github/workflows/test.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ jobs:
2626
- name: Install dependencies
2727
run: bun install --frozen-lockfile
2828

29+
- name: Check dependency baseline
30+
run: bun run deps:check
31+
2932
- name: Type check
3033
run: bunx tsc --noEmit
3134

deps-baseline.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"dependencies": {
3+
"@modelcontextprotocol/sdk": "^1.26.0",
4+
"zod": "^3.24.0"
5+
}
6+
}

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@aelfscan/agent-skills",
3-
"version": "0.2.0",
3+
"version": "0.2.1",
44
"description": "AelfScan explorer skill toolkit for AI agents: MCP, CLI, and SDK interfaces.",
55
"type": "module",
66
"main": "index.ts",
@@ -38,7 +38,8 @@
3838
"test:integration": "bun test tests/integration/",
3939
"test:e2e": "bun test tests/e2e/",
4040
"coverage:gate": "bun run scripts/coverage-gate.ts",
41-
"test:coverage:ci": "COVERAGE_MIN_LINES=85 COVERAGE_MIN_FUNCS=80 bun run test:unit:coverage && bun run coverage:gate"
41+
"test:coverage:ci": "COVERAGE_MIN_LINES=85 COVERAGE_MIN_FUNCS=80 bun run test:unit:coverage && bun run coverage:gate",
42+
"deps:check": "bun run scripts/check-deps-baseline.ts"
4243
},
4344
"keywords": [
4445
"aelfscan",

scripts/check-deps-baseline.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#!/usr/bin/env bun
2+
import { existsSync, readFileSync } from 'node:fs';
3+
import { resolve } from 'node:path';
4+
5+
type Baseline = {
6+
dependencies: Record<string, string>;
7+
};
8+
9+
function readJson<T>(filePath: string): T {
10+
return JSON.parse(readFileSync(filePath, 'utf8')) as T;
11+
}
12+
13+
function main() {
14+
const cwd = process.cwd();
15+
const baselinePath = resolve(cwd, 'deps-baseline.json');
16+
const packagePath = resolve(cwd, 'package.json');
17+
18+
if (!existsSync(baselinePath)) {
19+
console.error(`[deps:check] missing deps-baseline.json at ${baselinePath}`);
20+
process.exit(1);
21+
}
22+
23+
if (!existsSync(packagePath)) {
24+
console.error(`[deps:check] missing package.json at ${packagePath}`);
25+
process.exit(1);
26+
}
27+
28+
const baseline = readJson<Baseline>(baselinePath);
29+
const pkg = readJson<any>(packagePath);
30+
const declaredDeps = {
31+
...(pkg.dependencies || {}),
32+
...(pkg.devDependencies || {}),
33+
} as Record<string, string>;
34+
35+
const failures: string[] = [];
36+
for (const [name, expected] of Object.entries(baseline.dependencies || {})) {
37+
const actual = declaredDeps[name];
38+
if (!actual) {
39+
failures.push(`${name}: missing (expected ${expected})`);
40+
continue;
41+
}
42+
if (actual !== expected) {
43+
failures.push(`${name}: expected ${expected}, got ${actual}`);
44+
}
45+
}
46+
47+
if (failures.length > 0) {
48+
console.error('[deps:check] dependency baseline mismatch:');
49+
for (const failure of failures) {
50+
console.error(`- ${failure}`);
51+
}
52+
process.exit(1);
53+
}
54+
55+
console.log('[deps:check] passed');
56+
}
57+
58+
main();

src/mcp/output.ts

Lines changed: 18 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -1,128 +1,9 @@
11
import { getConfig } from '../../lib/config.js';
22
import type { ToolOutputPolicy } from '../tooling/tool-descriptors.js';
3-
4-
interface TruncationMeta {
5-
truncated: boolean;
6-
maxItems: number;
7-
maxChars: number;
8-
originalSizeEstimate: number;
9-
}
10-
11-
function isRecord(value: unknown): value is Record<string, unknown> {
12-
return typeof value === 'object' && value !== null && !Array.isArray(value);
13-
}
14-
15-
function summarizeValue(value: unknown): Record<string, unknown> {
16-
if (Array.isArray(value)) {
17-
return {
18-
type: 'array',
19-
length: value.length,
20-
preview: value.slice(0, 3),
21-
};
22-
}
23-
24-
if (isRecord(value)) {
25-
const keys = Object.keys(value);
26-
return {
27-
type: 'object',
28-
keys,
29-
keyCount: keys.length,
30-
};
31-
}
32-
33-
return {
34-
type: typeof value,
35-
value,
36-
};
37-
}
38-
39-
function truncateArrays(value: unknown, maxItems: number): { value: unknown; truncated: boolean } {
40-
if (Array.isArray(value)) {
41-
const truncatedItems = value.slice(0, maxItems).map(item => truncateArrays(item, maxItems));
42-
const wasTruncated = value.length > maxItems || truncatedItems.some(item => item.truncated);
43-
44-
return {
45-
value: truncatedItems.map(item => item.value),
46-
truncated: wasTruncated,
47-
};
48-
}
49-
50-
if (isRecord(value)) {
51-
let truncated = false;
52-
const next: Record<string, unknown> = {};
53-
54-
for (const [key, itemValue] of Object.entries(value)) {
55-
const child = truncateArrays(itemValue, maxItems);
56-
if (child.truncated) {
57-
truncated = true;
58-
}
59-
next[key] = child.value;
60-
}
61-
62-
return {
63-
value: next,
64-
truncated,
65-
};
66-
}
67-
68-
return {
69-
value,
70-
truncated: false,
71-
};
72-
}
73-
74-
function stripRawByConfig(value: unknown, includeRaw: boolean): unknown {
75-
if (includeRaw) {
76-
return value;
77-
}
78-
79-
if (!isRecord(value)) {
80-
return value;
81-
}
82-
83-
const next = { ...value };
84-
delete next.raw;
85-
return next;
86-
}
87-
88-
function attachMeta(value: unknown, meta: TruncationMeta): unknown {
89-
if (isRecord(value)) {
90-
return {
91-
...value,
92-
meta,
93-
};
94-
}
95-
96-
return {
97-
data: value,
98-
meta,
99-
};
100-
}
101-
102-
function safeSerialize(value: unknown): string {
103-
try {
104-
return JSON.stringify(value, null, 2);
105-
} catch {
106-
return JSON.stringify({ data: summarizeValue(value) }, null, 2);
107-
}
108-
}
109-
110-
function shrinkForMaxChars(value: unknown, meta: TruncationMeta): unknown {
111-
if (isRecord(value)) {
112-
return {
113-
success: value.success,
114-
traceId: value.traceId,
115-
dataSummary: summarizeValue(value.data),
116-
error: value.error,
117-
meta,
118-
};
119-
}
120-
121-
return {
122-
dataSummary: summarizeValue(value),
123-
meta,
124-
};
125-
}
3+
import {
4+
applyOutputGovernance,
5+
applySummaryPolicy as applySummaryPolicyFields,
6+
} from '../tooling/mcp-output-governance.js';
1267

1278
function applySummaryPolicy(
1289
value: unknown,
@@ -133,60 +14,31 @@ function applySummaryPolicy(
13314
return { value, truncated: false };
13415
}
13516

136-
if (!isRecord(value) || !isRecord(value.data)) {
137-
return { value, truncated: false };
138-
}
139-
140-
const data = value.data as Record<string, unknown>;
141-
const nextData = { ...data };
142-
let truncated = false;
143-
144-
for (const key of ['list', 'items', 'blocks', 'transactions', 'logEvents']) {
145-
const item = nextData[key];
146-
if (Array.isArray(item) && item.length > maxItems) {
147-
nextData[key] = item.slice(0, maxItems);
148-
truncated = true;
149-
}
150-
}
151-
152-
return {
153-
value: {
154-
...value,
155-
data: nextData,
156-
},
157-
truncated,
158-
};
17+
return applySummaryPolicyFields(value, maxItems);
15918
}
16019

16120
export function asMcpResult(data: unknown, outputPolicy: ToolOutputPolicy = 'normal') {
16221
const config = getConfig();
16322
const maxItems = Math.max(1, config.mcpMaxItems);
16423
const maxChars = Math.max(1, config.mcpMaxChars);
16524

166-
const stripped = stripRawByConfig(data, config.mcpIncludeRaw);
167-
const summaryApplied = applySummaryPolicy(stripped, outputPolicy, maxItems);
168-
const truncatedArrays = truncateArrays(summaryApplied.value, maxItems);
169-
170-
const initialMeta: TruncationMeta = {
171-
truncated: summaryApplied.truncated || truncatedArrays.truncated,
25+
const summaryApplied = applySummaryPolicy(data, outputPolicy, maxItems);
26+
const governed = applyOutputGovernance(summaryApplied.value, {
17227
maxItems,
17328
maxChars,
174-
originalSizeEstimate: safeSerialize(truncatedArrays.value).length,
175-
};
176-
177-
const meta: TruncationMeta = initialMeta;
178-
let payload = attachMeta(truncatedArrays.value, meta);
179-
let serialized = safeSerialize(payload);
180-
181-
if (serialized.length > maxChars) {
182-
const reducedMeta: TruncationMeta = {
183-
...meta,
184-
truncated: true,
29+
includeRaw: config.mcpIncludeRaw,
30+
});
31+
let payload = governed.payload as Record<string, unknown>;
32+
if (summaryApplied.truncated && payload?.meta && typeof payload.meta === 'object') {
33+
payload = {
34+
...payload,
35+
meta: {
36+
...(payload.meta as Record<string, unknown>),
37+
truncated: true,
38+
},
18539
};
186-
187-
payload = shrinkForMaxChars(truncatedArrays.value, reducedMeta);
188-
serialized = safeSerialize(payload);
18940
}
41+
const serialized = JSON.stringify(payload, null, 2);
19042

19143
return {
19244
content: [

0 commit comments

Comments
 (0)