@foundation0/api 1.1.2 → 1.1.4
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/agents.ts +7 -6
- package/git.ts +2 -2
- package/libs/curl.test.ts +130 -0
- package/libs/curl.ts +770 -0
- package/mcp/AGENTS.md +130 -0
- package/mcp/server.test.ts +464 -250
- package/mcp/server.ts +2449 -1673
- package/net.ts +170 -0
- package/package.json +5 -3
- package/projects.ts +582 -79
package/agents.ts
CHANGED
|
@@ -39,7 +39,7 @@ interface ActiveConfigInput {
|
|
|
39
39
|
required: boolean
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
const CLI_NAME = '
|
|
42
|
+
const CLI_NAME = 'f0'
|
|
43
43
|
const AGENT_INITIAL_VERSION = 'v0.0.1'
|
|
44
44
|
const DEFAULT_SKILL_NAME = 'coding-standards'
|
|
45
45
|
const VERSION_RE = 'v?\\d+(?:\\.\\d+){2,}'
|
|
@@ -58,7 +58,7 @@ export function usage(): string {
|
|
|
58
58
|
`Use --latest to resolve /file-name to the latest version.\n` +
|
|
59
59
|
`The active file created is [file].active.<ext>.\n` +
|
|
60
60
|
`Use "run" to start codex with developer_instructions from system/prompt.ts.\n` +
|
|
61
|
-
`Set EXAMPLE_CODEX_BIN to pin a specific codex binary.\n`
|
|
61
|
+
`Set F0_CODEX_BIN (or EXAMPLE_CODEX_BIN) to pin a specific codex binary.\n`
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
export function resolveAgentsRoot(processRoot: string = process.cwd()): string {
|
|
@@ -460,9 +460,9 @@ export function setActiveLink(sourceFile: string, activeFile: string): 'symlink'
|
|
|
460
460
|
|
|
461
461
|
fs.mkdirSync(path.dirname(activeFile), { recursive: true })
|
|
462
462
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
}
|
|
463
|
+
// Remove the active pointer first. Using rmSync directly (instead of existsSync)
|
|
464
|
+
// ensures we also clear broken symlinks (existsSync returns false for them).
|
|
465
|
+
fs.rmSync(activeFile, { force: true })
|
|
466
466
|
|
|
467
467
|
const linkTarget = path.relative(path.dirname(activeFile), sourceFile)
|
|
468
468
|
|
|
@@ -643,7 +643,7 @@ type SpawnCodexResult = {
|
|
|
643
643
|
type SpawnCodexFn = (args: string[]) => SpawnCodexResult
|
|
644
644
|
|
|
645
645
|
function getCodexCommand(): string {
|
|
646
|
-
const override = process.env.EXAMPLE_CODEX_BIN?.trim()
|
|
646
|
+
const override = process.env.F0_CODEX_BIN?.trim() ?? process.env.EXAMPLE_CODEX_BIN?.trim()
|
|
647
647
|
if (override) {
|
|
648
648
|
return override
|
|
649
649
|
}
|
|
@@ -666,6 +666,7 @@ const defaultSpawnCodex: SpawnCodexFn = (args) => {
|
|
|
666
666
|
const err = primary.error as NodeJS.ErrnoException
|
|
667
667
|
if (
|
|
668
668
|
process.platform === 'win32'
|
|
669
|
+
&& !process.env.F0_CODEX_BIN
|
|
669
670
|
&& !process.env.EXAMPLE_CODEX_BIN
|
|
670
671
|
&& primaryCommand === 'codex.cmd'
|
|
671
672
|
&& err.code === 'ENOENT'
|
package/git.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type {
|
|
|
5
5
|
GitServiceApi,
|
|
6
6
|
GitServiceApiExecutionResult,
|
|
7
7
|
GitServiceApiMethod,
|
|
8
|
-
} from '
|
|
8
|
+
} from '@foundation0/git'
|
|
9
9
|
|
|
10
10
|
export {
|
|
11
11
|
attachGitLabelManagementApi,
|
|
@@ -17,4 +17,4 @@ export {
|
|
|
17
17
|
extractDependencyIssueNumbers,
|
|
18
18
|
resolveProjectRepoIdentity,
|
|
19
19
|
syncIssueDependencies,
|
|
20
|
-
} from '
|
|
20
|
+
} from '@foundation0/git'
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import { curl } from './curl'
|
|
3
|
+
|
|
4
|
+
describe('api/libs/curl', () => {
|
|
5
|
+
const withServer = async <T>(fn: (baseUrl: string) => Promise<T>): Promise<T> => {
|
|
6
|
+
const server = Bun.serve({
|
|
7
|
+
port: 0,
|
|
8
|
+
fetch: async (req) => {
|
|
9
|
+
const url = new URL(req.url)
|
|
10
|
+
|
|
11
|
+
if (url.pathname === '/hello') {
|
|
12
|
+
return new Response('hello', { headers: { 'x-test': '1' } })
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (url.pathname === '/echo') {
|
|
16
|
+
const body = await req.text()
|
|
17
|
+
return Response.json({
|
|
18
|
+
method: req.method,
|
|
19
|
+
body,
|
|
20
|
+
contentType: req.headers.get('content-type'),
|
|
21
|
+
ua: req.headers.get('user-agent'),
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (url.pathname === '/redirect') {
|
|
26
|
+
return new Response('nope', { status: 302, headers: { location: '/hello' } })
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return new Response('not found', { status: 404 })
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
return await fn(`http://127.0.0.1:${server.port}`)
|
|
35
|
+
} finally {
|
|
36
|
+
server.stop(true)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
it('fetches a URL and returns body on stdout', async () => {
|
|
41
|
+
await withServer(async (baseUrl) => {
|
|
42
|
+
const result = await curl(`${baseUrl}/hello`)
|
|
43
|
+
expect(result.exitCode).toBe(0)
|
|
44
|
+
expect(result.timedOut).toBe(false)
|
|
45
|
+
expect(result.stdout).toBe('hello')
|
|
46
|
+
expect(result.results?.[0]?.httpCode).toBe(200)
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('defaults scheme-less host URLs to http://', async () => {
|
|
51
|
+
await withServer(async (baseUrl) => {
|
|
52
|
+
const url = baseUrl.replace('http://', '')
|
|
53
|
+
const result = await curl(`${url}/hello`)
|
|
54
|
+
expect(result.exitCode).toBe(0)
|
|
55
|
+
expect(result.stdout).toBe('hello')
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('supports -i to include response headers', async () => {
|
|
60
|
+
await withServer(async (baseUrl) => {
|
|
61
|
+
const result = await curl('-i', `${baseUrl}/hello`)
|
|
62
|
+
expect(result.exitCode).toBe(0)
|
|
63
|
+
expect(result.stdout).toContain('HTTP/1.1 200')
|
|
64
|
+
expect(result.stdout.toLowerCase()).toContain('x-test: 1')
|
|
65
|
+
expect(result.stdout).toContain('hello')
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('supports -d for POST body and defaults method to POST', async () => {
|
|
70
|
+
await withServer(async (baseUrl) => {
|
|
71
|
+
const result = await curl('-d', 'a=1', `${baseUrl}/echo`)
|
|
72
|
+
expect(result.exitCode).toBe(0)
|
|
73
|
+
const payload = JSON.parse(result.stdout)
|
|
74
|
+
expect(payload.method).toBe('POST')
|
|
75
|
+
expect(payload.body).toBe('a=1')
|
|
76
|
+
expect(payload.contentType).toContain('application/x-www-form-urlencoded')
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('supports -G with -d to apply data as query params', async () => {
|
|
81
|
+
await withServer(async (baseUrl) => {
|
|
82
|
+
const result = await curl('-G', '-d', 'a=1', `${baseUrl}/hello`)
|
|
83
|
+
expect(result.exitCode).toBe(0)
|
|
84
|
+
expect(result.stdout).toBe('hello')
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('supports -L to follow redirects', async () => {
|
|
89
|
+
await withServer(async (baseUrl) => {
|
|
90
|
+
const result = await curl('-L', `${baseUrl}/redirect`)
|
|
91
|
+
expect(result.exitCode).toBe(0)
|
|
92
|
+
expect(result.results?.[0]?.redirectCount).toBe(1)
|
|
93
|
+
expect(result.results?.[0]?.urlEffective).toContain('/hello')
|
|
94
|
+
expect(result.stdout).toBe('hello')
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('supports -f to fail on HTTP >= 400 and suppress body', async () => {
|
|
99
|
+
await withServer(async (baseUrl) => {
|
|
100
|
+
const result = await curl('-f', `${baseUrl}/missing`)
|
|
101
|
+
expect(result.exitCode).toBe(22)
|
|
102
|
+
expect(result.stdout).toBe('')
|
|
103
|
+
expect(result.stderr).toContain('returned error')
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('supports -w to write out %{http_code}', async () => {
|
|
108
|
+
await withServer(async (baseUrl) => {
|
|
109
|
+
const result = await curl('-w', '%{http_code}', `${baseUrl}/hello`)
|
|
110
|
+
expect(result.exitCode).toBe(0)
|
|
111
|
+
expect(result.stdout).toBe('hello200')
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('accepts trailing tool options object without turning it into a curl arg', async () => {
|
|
116
|
+
await withServer(async (baseUrl) => {
|
|
117
|
+
const result = await curl(`${baseUrl}/hello`, { timeoutMs: 10_000 })
|
|
118
|
+
expect(result.exitCode).toBe(0)
|
|
119
|
+
expect(result.args).toEqual([`${baseUrl}/hello`])
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('returns usage exit code for unsupported flags', async () => {
|
|
124
|
+
await withServer(async (baseUrl) => {
|
|
125
|
+
const result = await curl('--version', `${baseUrl}/hello`)
|
|
126
|
+
expect(result.exitCode).toBe(2)
|
|
127
|
+
expect(result.stderr).toContain('Unsupported curl option')
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
})
|