@flowcodex/core 0.3.0
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/LICENSE +21 -0
- package/README.md +9 -0
- package/dist/index-LbxYtxxS.d.ts +560 -0
- package/dist/index.d.ts +995 -0
- package/dist/index.js +3840 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel/index.d.ts +1 -0
- package/dist/kernel/index.js +551 -0
- package/dist/kernel/index.js.map +1 -0
- package/package.json +39 -0
- package/src/agent/agent-loop.ts +254 -0
- package/src/agent/context.ts +99 -0
- package/src/agent/conversation-state.ts +44 -0
- package/src/agent/provider-runner.ts +241 -0
- package/src/agent/system-prompt-builder.ts +193 -0
- package/src/execution/compactor.ts +256 -0
- package/src/execution/index.ts +7 -0
- package/src/execution/output-serializer.ts +90 -0
- package/src/execution/schema-validator.ts +124 -0
- package/src/execution/tool-executor.ts +276 -0
- package/src/execution/tool-registry.ts +104 -0
- package/src/index.ts +215 -0
- package/src/infrastructure/catalog-parser.ts +218 -0
- package/src/infrastructure/index.ts +16 -0
- package/src/infrastructure/path-resolver.ts +123 -0
- package/src/infrastructure/provider-factory.ts +116 -0
- package/src/infrastructure/provider-presets.ts +19 -0
- package/src/infrastructure/retry-policy.ts +50 -0
- package/src/infrastructure/secret-scrubber.ts +67 -0
- package/src/infrastructure/token-counter.ts +156 -0
- package/src/infrastructure/tracer.ts +23 -0
- package/src/kernel/container.ts +166 -0
- package/src/kernel/events.ts +323 -0
- package/src/kernel/index.ts +18 -0
- package/src/kernel/pipeline.ts +152 -0
- package/src/kernel/run-controller.ts +85 -0
- package/src/kernel/tokens.ts +21 -0
- package/src/security/index.ts +13 -0
- package/src/security/permission-policy.ts +273 -0
- package/src/session/audit-log.ts +201 -0
- package/src/session/auth-service.ts +178 -0
- package/src/session/index.ts +26 -0
- package/src/session/secret-vault.ts +183 -0
- package/src/session/session-store.ts +339 -0
- package/src/session/types.ts +100 -0
- package/src/types/blocks.ts +56 -0
- package/src/types/context.ts +54 -0
- package/src/types/errors.ts +359 -0
- package/src/types/index.ts +34 -0
- package/src/types/provider.ts +58 -0
- package/src/types/tool.ts +39 -0
- package/src/utils/error.ts +3 -0
- package/src/utils/fs.ts +185 -0
- package/src/utils/image-resize.ts +76 -0
- package/src/utils/ssrf-guard.ts +133 -0
- package/src/utils/ulid.ts +72 -0
- package/src/utils/version-check.ts +59 -0
- package/tests/agent-loop.test.ts +490 -0
- package/tests/audit-log.test.ts +199 -0
- package/tests/auth-service.test.ts +170 -0
- package/tests/blocks.test.ts +79 -0
- package/tests/catalog-parser.test.ts +174 -0
- package/tests/compactor.test.ts +180 -0
- package/tests/container.test.ts +224 -0
- package/tests/conversation-state.test.ts +75 -0
- package/tests/errors.test.ts +429 -0
- package/tests/events-v021.test.ts +60 -0
- package/tests/events-v022.test.ts +75 -0
- package/tests/events.test.ts +340 -0
- package/tests/fixtures/large-image.png +0 -0
- package/tests/fixtures/small-image.png +0 -0
- package/tests/fs-utils.test.ts +164 -0
- package/tests/image-resize.test.ts +51 -0
- package/tests/output-serializer.test.ts +79 -0
- package/tests/path-resolver.test.ts +91 -0
- package/tests/permission-policy.test.ts +174 -0
- package/tests/pipeline.test.ts +193 -0
- package/tests/provider-factory.test.ts +245 -0
- package/tests/provider-runner.test.ts +535 -0
- package/tests/retry-policy.test.ts +104 -0
- package/tests/run-controller.test.ts +115 -0
- package/tests/sanity.test.ts +26 -0
- package/tests/schema-validator.test.ts +109 -0
- package/tests/secret-scrubber.test.ts +133 -0
- package/tests/secret-vault.test.ts +130 -0
- package/tests/session-store.test.ts +429 -0
- package/tests/ssrf-guard.test.ts +112 -0
- package/tests/system-prompt-builder.test.ts +116 -0
- package/tests/token-counter.test.ts +163 -0
- package/tests/tokens.test.ts +42 -0
- package/tests/tool-executor.test.ts +452 -0
- package/tests/tool-registry.test.ts +143 -0
- package/tests/tracer.test.ts +32 -0
- package/tests/ulid.test.ts +53 -0
- package/tests/version-check.test.ts +57 -0
- package/tsconfig.json +11 -0
- package/tsup.config.ts +16 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ToolRegistry } from '../src/execution/tool-registry.js';
|
|
3
|
+
import type { Tool } from '../src/types/tool.js';
|
|
4
|
+
|
|
5
|
+
function makeTool(name: string): Tool {
|
|
6
|
+
return {
|
|
7
|
+
name,
|
|
8
|
+
description: `Test tool ${name}`,
|
|
9
|
+
inputSchema: { type: 'object', properties: { x: { type: 'string' } }, required: ['x'] },
|
|
10
|
+
permission: 'auto',
|
|
11
|
+
mutating: false,
|
|
12
|
+
async execute() {
|
|
13
|
+
return { ok: true };
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('ToolRegistry', () => {
|
|
19
|
+
it('should register and get a tool', () => {
|
|
20
|
+
const reg = new ToolRegistry();
|
|
21
|
+
const tool = makeTool('foo');
|
|
22
|
+
reg.register(tool, 'core');
|
|
23
|
+
expect(reg.get('foo')).toBe(tool);
|
|
24
|
+
expect(reg.has('foo')).toBe(true);
|
|
25
|
+
expect(reg.size).toBe(1);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should throw on duplicate registration', () => {
|
|
29
|
+
const reg = new ToolRegistry();
|
|
30
|
+
reg.register(makeTool('foo'), 'core');
|
|
31
|
+
expect(() => reg.register(makeTool('foo'), 'core')).toThrow('already registered');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('tryRegister should return false on conflict', () => {
|
|
35
|
+
const reg = new ToolRegistry();
|
|
36
|
+
expect(reg.tryRegister(makeTool('foo'), 'core')).toBe(true);
|
|
37
|
+
expect(reg.tryRegister(makeTool('foo'), 'core')).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('registerDefault should be no-op if exists', () => {
|
|
41
|
+
const reg = new ToolRegistry();
|
|
42
|
+
const t1 = makeTool('foo');
|
|
43
|
+
const t2 = makeTool('foo');
|
|
44
|
+
reg.registerDefault(t1, 'core');
|
|
45
|
+
reg.registerDefault(t2, 'plugin');
|
|
46
|
+
expect(reg.get('foo')).toBe(t1);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('registerAll should register multiple tools silently', () => {
|
|
50
|
+
const reg = new ToolRegistry();
|
|
51
|
+
reg.registerAll([makeTool('a'), makeTool('b'), makeTool('a')], 'core');
|
|
52
|
+
expect(reg.size).toBe(2);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('override should replace existing tool', () => {
|
|
56
|
+
const reg = new ToolRegistry();
|
|
57
|
+
const t1 = makeTool('foo');
|
|
58
|
+
const t2 = makeTool('foo');
|
|
59
|
+
reg.register(t1, 'core');
|
|
60
|
+
reg.override('foo', t2, 'plugin');
|
|
61
|
+
expect(reg.get('foo')).toBe(t2);
|
|
62
|
+
expect(reg.ownerOf('foo')).toBe('plugin');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('override should throw if not registered', () => {
|
|
66
|
+
const reg = new ToolRegistry();
|
|
67
|
+
expect(() => reg.override('missing', makeTool('missing'), 'core')).toThrow('not registered');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('list should return all tools', () => {
|
|
71
|
+
const reg = new ToolRegistry();
|
|
72
|
+
reg.register(makeTool('a'), 'core');
|
|
73
|
+
reg.register(makeTool('b'), 'core');
|
|
74
|
+
expect(reg.list().map((t) => t.name).sort()).toEqual(['a', 'b']);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('listWithOwner should include owner', () => {
|
|
78
|
+
const reg = new ToolRegistry();
|
|
79
|
+
reg.register(makeTool('a'), 'core');
|
|
80
|
+
reg.register(makeTool('b'), 'plugin');
|
|
81
|
+
const entries = reg.listWithOwner();
|
|
82
|
+
expect(entries).toHaveLength(2);
|
|
83
|
+
const aEntry = entries.find((e) => e.tool.name === 'a');
|
|
84
|
+
expect(aEntry?.owner).toBe('core');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('clone should produce independent copy', () => {
|
|
88
|
+
const reg = new ToolRegistry();
|
|
89
|
+
reg.register(makeTool('a'), 'core');
|
|
90
|
+
const copy = reg.clone();
|
|
91
|
+
copy.register(makeTool('b'), 'core');
|
|
92
|
+
expect(reg.size).toBe(1);
|
|
93
|
+
expect(copy.size).toBe(2);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('totalEstDefTokens should be positive', () => {
|
|
97
|
+
const reg = new ToolRegistry();
|
|
98
|
+
reg.register(makeTool('a'), 'core');
|
|
99
|
+
expect(reg.totalEstDefTokens).toBeGreaterThan(0);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('ToolRegistry.filter', () => {
|
|
104
|
+
function makeTieredTool(name: string, tier: 'essential' | 'extended' | undefined): Tool {
|
|
105
|
+
return {
|
|
106
|
+
name,
|
|
107
|
+
description: `Tool ${name}`,
|
|
108
|
+
inputSchema: { type: 'object' },
|
|
109
|
+
permission: 'auto',
|
|
110
|
+
mutating: false,
|
|
111
|
+
tier,
|
|
112
|
+
async execute() {
|
|
113
|
+
return {};
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
it('removes tools that do not match the predicate', () => {
|
|
119
|
+
const reg = new ToolRegistry();
|
|
120
|
+
reg.register(makeTieredTool('a', 'essential'), 'core');
|
|
121
|
+
reg.register(makeTieredTool('b', 'extended'), 'core');
|
|
122
|
+
reg.register(makeTieredTool('c', 'essential'), 'core');
|
|
123
|
+
const counts = reg.filter((t) => t.tier === 'essential');
|
|
124
|
+
expect(counts).toEqual({ total: 3, kept: 2 });
|
|
125
|
+
expect(reg.has('a')).toBe(true);
|
|
126
|
+
expect(reg.has('b')).toBe(false);
|
|
127
|
+
expect(reg.has('c')).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('returns zero counts for empty registry', () => {
|
|
131
|
+
const reg = new ToolRegistry();
|
|
132
|
+
const counts = reg.filter(() => true);
|
|
133
|
+
expect(counts).toEqual({ total: 0, kept: 0 });
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('keeps all when predicate is always true', () => {
|
|
137
|
+
const reg = new ToolRegistry();
|
|
138
|
+
reg.register(makeTieredTool('a', 'essential'), 'core');
|
|
139
|
+
reg.register(makeTieredTool('b', 'extended'), 'core');
|
|
140
|
+
const counts = reg.filter(() => true);
|
|
141
|
+
expect(counts).toEqual({ total: 2, kept: 2 });
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { NoopTracer, NoopSpan } from '../src/infrastructure/tracer.js';
|
|
3
|
+
|
|
4
|
+
describe('NoopTracer', () => {
|
|
5
|
+
it('startSpan returns a NoopSpan', () => {
|
|
6
|
+
const tracer = new NoopTracer();
|
|
7
|
+
const span = tracer.startSpan('test');
|
|
8
|
+
expect(span).toBeInstanceOf(NoopSpan);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('startSpan with attrs does not throw', () => {
|
|
12
|
+
const tracer = new NoopTracer();
|
|
13
|
+
expect(() => tracer.startSpan('test', { key: 'value' })).not.toThrow();
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('NoopSpan', () => {
|
|
18
|
+
it('setAttribute returns this (chainable)', () => {
|
|
19
|
+
const span = new NoopSpan();
|
|
20
|
+
expect(span.setAttribute('key', 'value')).toBe(span);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('recordError does not throw', () => {
|
|
24
|
+
const span = new NoopSpan();
|
|
25
|
+
expect(() => span.recordError(new Error('test'))).not.toThrow();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('end does not throw', () => {
|
|
29
|
+
const span = new NoopSpan();
|
|
30
|
+
expect(() => span.end()).not.toThrow();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ulid, ulidTime, isUlid } from '../src/utils/ulid.js';
|
|
3
|
+
|
|
4
|
+
describe('ulid', () => {
|
|
5
|
+
it('generates a 26-character string', () => {
|
|
6
|
+
const id = ulid();
|
|
7
|
+
expect(id).toHaveLength(26);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('generates valid ULID characters only', () => {
|
|
11
|
+
for (let i = 0; i < 100; i++) {
|
|
12
|
+
const id = ulid();
|
|
13
|
+
expect(isUlid(id)).toBe(true);
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('is sortable by time', () => {
|
|
18
|
+
const a = ulid(1700000000000);
|
|
19
|
+
const b = ulid(1700000000001);
|
|
20
|
+
expect(a < b).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('same millisecond produces monotonic increment', () => {
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
const a = ulid(now);
|
|
26
|
+
const b = ulid(now);
|
|
27
|
+
expect(a < b).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('different times produce different ids', () => {
|
|
31
|
+
const ids = new Set<string>();
|
|
32
|
+
for (let i = 0; i < 1000; i++) {
|
|
33
|
+
ids.add(ulid(1700000000000 + i));
|
|
34
|
+
}
|
|
35
|
+
expect(ids.size).toBe(1000);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('ulidTime extracts the timestamp', () => {
|
|
39
|
+
const now = 1700000000123;
|
|
40
|
+
const id = ulid(now);
|
|
41
|
+
expect(ulidTime(id)).toBe(now);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('isUlid returns false for invalid strings', () => {
|
|
45
|
+
expect(isUlid('short')).toBe(false);
|
|
46
|
+
expect(isUlid('0O0I0O0I0O0O0I0O0I0O0I0O0I')).toBe(false); // contains O and I
|
|
47
|
+
expect(isUlid('')).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('ulidTime throws for invalid ULID', () => {
|
|
51
|
+
expect(() => ulidTime('invalid')).toThrow();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { compareVersions, fetchLatestVersion, checkForUpdate } from '../src/utils/version-check.js';
|
|
3
|
+
|
|
4
|
+
describe('compareVersions', () => {
|
|
5
|
+
it('equal versions return 0', () => {
|
|
6
|
+
expect(compareVersions('1.0.0', '1.0.0')).toBe(0);
|
|
7
|
+
});
|
|
8
|
+
it('older current returns -1', () => {
|
|
9
|
+
expect(compareVersions('1.0.0', '1.0.1')).toBe(-1);
|
|
10
|
+
});
|
|
11
|
+
it('newer current returns 1', () => {
|
|
12
|
+
expect(compareVersions('2.0.0', '1.0.0')).toBe(1);
|
|
13
|
+
});
|
|
14
|
+
it('handles different segment counts', () => {
|
|
15
|
+
expect(compareVersions('1.0', '1.0.1')).toBe(-1);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('fetchLatestVersion', () => {
|
|
20
|
+
it('returns version from successful response', async () => {
|
|
21
|
+
const mockFetch = async (): Promise<Response> =>
|
|
22
|
+
new Response(JSON.stringify({ version: '0.3.0' }), { status: 200 }) as Response;
|
|
23
|
+
const result = await fetchLatestVersion('@flowcodex/cli', { fetchImpl: mockFetch as typeof fetch });
|
|
24
|
+
expect(result).toBe('0.3.0');
|
|
25
|
+
});
|
|
26
|
+
it('returns undefined on 404', async () => {
|
|
27
|
+
const mockFetch = async (): Promise<Response> => new Response('Not Found', { status: 404 }) as Response;
|
|
28
|
+
const result = await fetchLatestVersion('@flowcodex/cli', { fetchImpl: mockFetch as typeof fetch });
|
|
29
|
+
expect(result).toBeUndefined();
|
|
30
|
+
});
|
|
31
|
+
it('returns undefined on network error', async () => {
|
|
32
|
+
const mockFetch = async (): Promise<Response> => { throw new Error('network'); };
|
|
33
|
+
const result = await fetchLatestVersion('@flowcodex/cli', { fetchImpl: mockFetch as typeof fetch });
|
|
34
|
+
expect(result).toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('checkForUpdate', () => {
|
|
39
|
+
it('returns updateAvailable true when latest is newer', async () => {
|
|
40
|
+
const mockFetch = async (): Promise<Response> =>
|
|
41
|
+
new Response(JSON.stringify({ version: '0.3.0' }), { status: 200 }) as Response;
|
|
42
|
+
const info = await checkForUpdate('0.2.2', '@flowcodex/cli', { fetchImpl: mockFetch as typeof fetch });
|
|
43
|
+
expect(info?.updateAvailable).toBe(true);
|
|
44
|
+
expect(info?.latest).toBe('0.3.0');
|
|
45
|
+
});
|
|
46
|
+
it('returns updateAvailable false when current', async () => {
|
|
47
|
+
const mockFetch = async (): Promise<Response> =>
|
|
48
|
+
new Response(JSON.stringify({ version: '0.2.2' }), { status: 200 }) as Response;
|
|
49
|
+
const info = await checkForUpdate('0.2.2', '@flowcodex/cli', { fetchImpl: mockFetch as typeof fetch });
|
|
50
|
+
expect(info?.updateAvailable).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
it('returns undefined when fetch fails', async () => {
|
|
53
|
+
const mockFetch = async (): Promise<Response> => { throw new Error('network'); };
|
|
54
|
+
const info = await checkForUpdate('0.2.2', '@flowcodex/cli', { fetchImpl: mockFetch as typeof fetch });
|
|
55
|
+
expect(info).toBeUndefined();
|
|
56
|
+
});
|
|
57
|
+
});
|
package/tsconfig.json
ADDED
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineConfig } from 'tsup';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: {
|
|
5
|
+
index: 'src/index.ts',
|
|
6
|
+
'kernel/index': 'src/kernel/index.ts',
|
|
7
|
+
},
|
|
8
|
+
format: ['esm'],
|
|
9
|
+
target: 'es2024',
|
|
10
|
+
platform: 'node',
|
|
11
|
+
dts: true,
|
|
12
|
+
clean: true,
|
|
13
|
+
sourcemap: true,
|
|
14
|
+
treeshake: true,
|
|
15
|
+
splitting: false,
|
|
16
|
+
});
|