@aliou/pi-ts-aperture 0.0.1
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/.changeset/config.json +10 -0
- package/.envrc +21 -0
- package/.github/workflows/ci.yml +31 -0
- package/.github/workflows/publish.yml +86 -0
- package/.osgrep/cache/meta.lmdb +0 -0
- package/.osgrep/cache/meta.lmdb-lock +0 -0
- package/.osgrep/lancedb/chunks.lance/_indices/b9016ebf-b6e6-4c43-be9f-7413728f6389/metadata.lance +0 -0
- package/.osgrep/lancedb/chunks.lance/_indices/b9016ebf-b6e6-4c43-be9f-7413728f6389/part_11_docs.lance +0 -0
- package/.osgrep/lancedb/chunks.lance/_indices/b9016ebf-b6e6-4c43-be9f-7413728f6389/part_11_invert.lance +0 -0
- package/.osgrep/lancedb/chunks.lance/_indices/b9016ebf-b6e6-4c43-be9f-7413728f6389/part_11_tokens.lance +0 -0
- package/.osgrep/lancedb/chunks.lance/_indices/cc7bd185-bc88-4dbd-af7f-6aac2f6b29d3/metadata.lance +0 -0
- package/.osgrep/lancedb/chunks.lance/_indices/cc7bd185-bc88-4dbd-af7f-6aac2f6b29d3/part_0_docs.lance +0 -0
- package/.osgrep/lancedb/chunks.lance/_indices/cc7bd185-bc88-4dbd-af7f-6aac2f6b29d3/part_0_invert.lance +0 -0
- package/.osgrep/lancedb/chunks.lance/_indices/cc7bd185-bc88-4dbd-af7f-6aac2f6b29d3/part_0_tokens.lance +0 -0
- package/.osgrep/lancedb/chunks.lance/_indices/cd01c8d9-fae8-4482-b01f-59f06abe1b48/metadata.lance +0 -0
- package/.osgrep/lancedb/chunks.lance/_indices/cd01c8d9-fae8-4482-b01f-59f06abe1b48/part_0_docs.lance +0 -0
- package/.osgrep/lancedb/chunks.lance/_indices/cd01c8d9-fae8-4482-b01f-59f06abe1b48/part_0_invert.lance +0 -0
- package/.osgrep/lancedb/chunks.lance/_indices/cd01c8d9-fae8-4482-b01f-59f06abe1b48/part_0_tokens.lance +0 -0
- package/.osgrep/lancedb/chunks.lance/_indices/dedba042-4a7b-44d7-ac2f-258233b62681/metadata.lance +0 -0
- package/.osgrep/lancedb/chunks.lance/_indices/dedba042-4a7b-44d7-ac2f-258233b62681/part_0_docs.lance +0 -0
- package/.osgrep/lancedb/chunks.lance/_indices/dedba042-4a7b-44d7-ac2f-258233b62681/part_0_invert.lance +0 -0
- package/.osgrep/lancedb/chunks.lance/_indices/dedba042-4a7b-44d7-ac2f-258233b62681/part_0_tokens.lance +0 -0
- package/.osgrep/lancedb/chunks.lance/_transactions/0-fd59fbcc-e229-4069-b8b0-764b34f2d645.txn +0 -0
- package/.osgrep/lancedb/chunks.lance/_transactions/1-f130f13d-123c-4e71-b806-b6b4f6cdd6d8.txn +0 -0
- package/.osgrep/lancedb/chunks.lance/_transactions/2-5f641c2a-07a9-45a1-b450-72c7d4d38754.txn +1 -0
- package/.osgrep/lancedb/chunks.lance/_transactions/3-83167775-6ec4-4cb6-9388-6549f583087a.txn +0 -0
- package/.osgrep/lancedb/chunks.lance/_transactions/4-feb1627d-f860-4c02-b8a6-6b93cf42ec22.txn +0 -0
- package/.osgrep/lancedb/chunks.lance/_transactions/5-e00ef82b-90b4-4c91-a7ec-d7b4903e1c61.txn +0 -0
- package/.osgrep/lancedb/chunks.lance/_transactions/6-ccc1d487-f511-4e48-afa0-2edf42f8effd.txn +0 -0
- package/.osgrep/lancedb/chunks.lance/_transactions/7-b26d7d18-4411-44cd-b559-54bfa2999b70.txn +0 -0
- package/.osgrep/lancedb/chunks.lance/_versions/1.manifest +0 -0
- package/.osgrep/lancedb/chunks.lance/_versions/2.manifest +0 -0
- package/.osgrep/lancedb/chunks.lance/_versions/3.manifest +0 -0
- package/.osgrep/lancedb/chunks.lance/_versions/4.manifest +0 -0
- package/.osgrep/lancedb/chunks.lance/_versions/5.manifest +0 -0
- package/.osgrep/lancedb/chunks.lance/_versions/6.manifest +0 -0
- package/.osgrep/lancedb/chunks.lance/_versions/7.manifest +0 -0
- package/.osgrep/lancedb/chunks.lance/_versions/8.manifest +0 -0
- package/.osgrep/lancedb/chunks.lance/data/0110110100011111000100108ca476475fac277950ba4295de.lance +0 -0
- package/.osgrep/lancedb/chunks.lance/data/111110011111110001101001d059cb4ceab8147482b4391458.lance +0 -0
- package/AGENTS.md +29 -0
- package/README.md +44 -0
- package/biome.json +30 -0
- package/package.json +54 -0
- package/shell.nix +10 -0
- package/src/commands/settings.ts +96 -0
- package/src/commands/setup.ts +204 -0
- package/src/config.ts +29 -0
- package/src/index.ts +39 -0
- package/tsconfig.json +17 -0
package/.envrc
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# shellcheck disable=2148,2086
|
|
2
|
+
|
|
3
|
+
# Use `source_up` if part of a monorepo.
|
|
4
|
+
|
|
5
|
+
use nix
|
|
6
|
+
|
|
7
|
+
# Load layouts. This basically just sets up PATH and other env vars.
|
|
8
|
+
# Possible values are "node", "bun", and "uv".
|
|
9
|
+
#
|
|
10
|
+
# Options:
|
|
11
|
+
# --deny bin1,bin2,... Exclude binaries from local PATH, falling back to system.
|
|
12
|
+
# Useful when a local binary shadows a system one you need.
|
|
13
|
+
#
|
|
14
|
+
# Examples:
|
|
15
|
+
# layout node
|
|
16
|
+
# layout node --deny pi,prettier
|
|
17
|
+
# layout uv --deny python
|
|
18
|
+
|
|
19
|
+
if has node; then
|
|
20
|
+
layout node --deny pi
|
|
21
|
+
fi
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
check:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- name: Checkout
|
|
14
|
+
uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Setup Node
|
|
17
|
+
uses: actions/setup-node@v4
|
|
18
|
+
with:
|
|
19
|
+
node-version: 22
|
|
20
|
+
|
|
21
|
+
- name: Setup pnpm
|
|
22
|
+
uses: pnpm/action-setup@v4
|
|
23
|
+
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: pnpm install --frozen-lockfile
|
|
26
|
+
|
|
27
|
+
- name: Lint
|
|
28
|
+
run: pnpm run lint
|
|
29
|
+
|
|
30
|
+
- name: Typecheck
|
|
31
|
+
run: pnpm run typecheck
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_run:
|
|
5
|
+
workflows: ["CI"]
|
|
6
|
+
types: [completed]
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
|
|
10
|
+
cancel-in-progress: true
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
publish:
|
|
14
|
+
name: Publish
|
|
15
|
+
if: >-
|
|
16
|
+
github.event.workflow_run.conclusion == 'success' &&
|
|
17
|
+
github.event.workflow_run.head_branch == 'main'
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
permissions:
|
|
20
|
+
contents: write
|
|
21
|
+
packages: write
|
|
22
|
+
pull-requests: write
|
|
23
|
+
id-token: write
|
|
24
|
+
|
|
25
|
+
steps:
|
|
26
|
+
- name: Checkout
|
|
27
|
+
uses: actions/checkout@v4
|
|
28
|
+
with:
|
|
29
|
+
ref: ${{ github.event.workflow_run.head_sha }}
|
|
30
|
+
fetch-depth: 0
|
|
31
|
+
|
|
32
|
+
- name: Setup pnpm
|
|
33
|
+
uses: pnpm/action-setup@v4
|
|
34
|
+
|
|
35
|
+
- name: Setup Node.js
|
|
36
|
+
uses: actions/setup-node@v4
|
|
37
|
+
with:
|
|
38
|
+
node-version: "22"
|
|
39
|
+
registry-url: "https://registry.npmjs.org"
|
|
40
|
+
scope: "@aliou"
|
|
41
|
+
cache: "pnpm"
|
|
42
|
+
|
|
43
|
+
- name: Upgrade npm for OIDC support
|
|
44
|
+
run: npm install -g npm@latest
|
|
45
|
+
|
|
46
|
+
- name: Install dependencies
|
|
47
|
+
run: pnpm install --frozen-lockfile
|
|
48
|
+
|
|
49
|
+
- name: Get release info
|
|
50
|
+
id: release-info
|
|
51
|
+
run: |
|
|
52
|
+
pnpm changeset status --output=release.json 2>/dev/null || echo '{"releases":[]}' > release.json
|
|
53
|
+
node <<NODE
|
|
54
|
+
const fs = require('fs');
|
|
55
|
+
const release = JSON.parse(fs.readFileSync('release.json', 'utf8'));
|
|
56
|
+
const releases = release.releases?.filter(r => r.type !== 'none') || [];
|
|
57
|
+
|
|
58
|
+
let title = 'Version Packages';
|
|
59
|
+
let commit = 'Version Packages';
|
|
60
|
+
if (releases.length === 1) {
|
|
61
|
+
const { name, newVersion } = releases[0];
|
|
62
|
+
title = 'Updating ' + name + ' to version ' + newVersion;
|
|
63
|
+
commit = name + '@' + newVersion;
|
|
64
|
+
} else if (releases.length > 1) {
|
|
65
|
+
const summary = releases.map(r => r.name + '@' + r.newVersion).join(', ');
|
|
66
|
+
title = 'Updating ' + summary;
|
|
67
|
+
commit = summary;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fs.appendFileSync(process.env.GITHUB_OUTPUT, 'title=' + title + '\n');
|
|
71
|
+
fs.appendFileSync(process.env.GITHUB_OUTPUT, 'commit=' + commit + '\n');
|
|
72
|
+
NODE
|
|
73
|
+
rm -f release.json
|
|
74
|
+
continue-on-error: true
|
|
75
|
+
|
|
76
|
+
- name: Create Release PR or Publish
|
|
77
|
+
id: changesets
|
|
78
|
+
uses: changesets/action@v1
|
|
79
|
+
with:
|
|
80
|
+
version: pnpm changeset version
|
|
81
|
+
publish: pnpm changeset publish
|
|
82
|
+
title: ${{ steps.release-info.outputs.title || 'Version Packages' }}
|
|
83
|
+
commit: ${{ steps.release-info.outputs.commit || 'Version Packages' }}
|
|
84
|
+
env:
|
|
85
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
86
|
+
NPM_CONFIG_PROVENANCE: true
|
|
Binary file
|
|
Binary file
|
package/.osgrep/lancedb/chunks.lance/_indices/b9016ebf-b6e6-4c43-be9f-7413728f6389/metadata.lance
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/.osgrep/lancedb/chunks.lance/_indices/cc7bd185-bc88-4dbd-af7f-6aac2f6b29d3/metadata.lance
ADDED
|
Binary file
|
package/.osgrep/lancedb/chunks.lance/_indices/cc7bd185-bc88-4dbd-af7f-6aac2f6b29d3/part_0_docs.lance
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/.osgrep/lancedb/chunks.lance/_indices/cd01c8d9-fae8-4482-b01f-59f06abe1b48/metadata.lance
ADDED
|
Binary file
|
package/.osgrep/lancedb/chunks.lance/_indices/cd01c8d9-fae8-4482-b01f-59f06abe1b48/part_0_docs.lance
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/.osgrep/lancedb/chunks.lance/_indices/dedba042-4a7b-44d7-ac2f-258233b62681/metadata.lance
ADDED
|
Binary file
|
package/.osgrep/lancedb/chunks.lance/_indices/dedba042-4a7b-44d7-ac2f-258233b62681/part_0_docs.lance
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
$5f641c2a-07a9-45a1-b450-72c7d4d38754�(&path IN ('tsconfig.json','biome.json')
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/.osgrep/lancedb/chunks.lance/data/0110110100011111000100108ca476475fac277950ba4295de.lance
ADDED
|
Binary file
|
package/.osgrep/lancedb/chunks.lance/data/111110011111110001101001d059cb4ceab8147482b4391458.lance
ADDED
|
Binary file
|
package/AGENTS.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# pi-ts-aperture
|
|
2
|
+
|
|
3
|
+
Pi extension that routes LLM providers through Tailscale Aperture.
|
|
4
|
+
|
|
5
|
+
## Structure
|
|
6
|
+
|
|
7
|
+
- `src/index.ts` - Entry point. Loads config, registers providers, registers commands.
|
|
8
|
+
- `src/config.ts` - Config schema (`ApertureConfig`, `ResolvedConfig`) and `ConfigLoader` instance.
|
|
9
|
+
- `src/commands/setup.ts` - `/aperture:setup` interactive wizard (URL input + provider multi-select).
|
|
10
|
+
- `src/commands/settings.ts` - `/aperture:settings` settings UI via `registerSettingsCommand`.
|
|
11
|
+
|
|
12
|
+
## Key decisions
|
|
13
|
+
|
|
14
|
+
- Config is global-only (no per-project scope). Aperture is a network-level concern.
|
|
15
|
+
- Provider list comes from `getProviders()` in `@mariozechner/pi-ai`, not hardcoded.
|
|
16
|
+
- `apiKey` is set to `"-"` because Aperture ignores client-provided keys.
|
|
17
|
+
- URLs are normalized on input: `http://` is prepended if missing, trailing `/v1` is stripped (appended at registration time).
|
|
18
|
+
|
|
19
|
+
## Dependencies
|
|
20
|
+
|
|
21
|
+
- `@aliou/pi-utils-settings` - Config loader and settings command infrastructure.
|
|
22
|
+
- `@mariozechner/pi-ai` - `getProviders()` for the known provider list.
|
|
23
|
+
- `@mariozechner/pi-coding-agent` - Extension API, `getSettingsListTheme`.
|
|
24
|
+
- `@mariozechner/pi-tui` - TUI components (`Input`, `Key`, `matchesKey`, `FuzzySelector`).
|
|
25
|
+
|
|
26
|
+
## Publishing
|
|
27
|
+
|
|
28
|
+
- Manual publish for 0.0.1, then changesets + GitHub Actions for subsequent versions.
|
|
29
|
+
- CI runs lint + typecheck on push/PR. Publish workflow triggers after CI succeeds on main.
|
package/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# pi-ts-aperture
|
|
2
|
+
|
|
3
|
+
Route Pi LLM providers through [Tailscale Aperture](https://tailscale.com/docs/features/aperture), a managed AI gateway on your tailnet.
|
|
4
|
+
|
|
5
|
+
Aperture handles API key injection and request routing server-side. This extension overrides the base URL for selected providers so all LLM requests go through your Aperture instance instead of directly to provider APIs.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pi install @aliou/pi-ts-aperture
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then run the setup wizard:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
/aperture:setup
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
This will prompt you for:
|
|
20
|
+
1. Your Aperture base URL (e.g. `ai.your-tailnet.ts.net`)
|
|
21
|
+
2. Which providers to route through Aperture (fuzzy searchable, multi-select)
|
|
22
|
+
|
|
23
|
+
Configuration is saved globally to `~/.pi/agent/extensions/aperture.json`.
|
|
24
|
+
|
|
25
|
+
## Commands
|
|
26
|
+
|
|
27
|
+
| Command | Description |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `/aperture:setup` | Interactive wizard to configure Aperture URL and providers |
|
|
30
|
+
| `/aperture:settings` | Settings UI to update base URL and provider list |
|
|
31
|
+
|
|
32
|
+
## How it works
|
|
33
|
+
|
|
34
|
+
For each configured provider, the extension calls `registerProvider` with:
|
|
35
|
+
- `baseUrl` set to your Aperture URL + `/v1`
|
|
36
|
+
- `apiKey` set to `"-"` (Aperture ignores client keys, it injects its own)
|
|
37
|
+
|
|
38
|
+
This means all requests for those providers are routed through Aperture, which handles authentication, logging, and cost tracking on its end.
|
|
39
|
+
|
|
40
|
+
## Requirements
|
|
41
|
+
|
|
42
|
+
- A Tailscale tailnet with Aperture configured
|
|
43
|
+
- The device running Pi must be on the tailnet
|
|
44
|
+
- Use HTTP, not HTTPS, for the Aperture URL (WireGuard handles encryption)
|
package/biome.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.4.2/schema.json",
|
|
3
|
+
"vcs": {
|
|
4
|
+
"enabled": true,
|
|
5
|
+
"clientKind": "git",
|
|
6
|
+
"useIgnoreFile": true
|
|
7
|
+
},
|
|
8
|
+
"files": {
|
|
9
|
+
"includes": ["**/*.ts", "**/*.json"],
|
|
10
|
+
"ignoreUnknown": true
|
|
11
|
+
},
|
|
12
|
+
"assist": {
|
|
13
|
+
"actions": {
|
|
14
|
+
"source": {
|
|
15
|
+
"organizeImports": "on"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"linter": {
|
|
20
|
+
"enabled": true,
|
|
21
|
+
"rules": {
|
|
22
|
+
"recommended": true
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"formatter": {
|
|
26
|
+
"enabled": true,
|
|
27
|
+
"indentStyle": "space",
|
|
28
|
+
"indentWidth": 2
|
|
29
|
+
}
|
|
30
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aliou/pi-ts-aperture",
|
|
3
|
+
"description": "Route Pi LLM providers through Tailscale Aperture",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"packageManager": "pnpm@10.26.1",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/aliou/pi-ts-aperture"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"pi-package"
|
|
12
|
+
],
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"pi": {
|
|
17
|
+
"extensions": [
|
|
18
|
+
"./src/index.ts"
|
|
19
|
+
],
|
|
20
|
+
"video": "https://assets.aliou.me/pi-extensions/demos/pi-ts-aperture.mp4"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@aliou/pi-utils-settings": "^0.3.0"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@mariozechner/pi-ai": ">=0.52.12",
|
|
27
|
+
"@mariozechner/pi-coding-agent": ">=0.52.12"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"lint": "biome check",
|
|
32
|
+
"format": "biome check --write",
|
|
33
|
+
"prepare": "husky",
|
|
34
|
+
"changeset": "changeset",
|
|
35
|
+
"version": "changeset version",
|
|
36
|
+
"release": "pnpm changeset publish"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@biomejs/biome": "^2.3.13",
|
|
40
|
+
"@changesets/cli": "^2.27.11",
|
|
41
|
+
"@mariozechner/pi-coding-agent": "0.52.12",
|
|
42
|
+
"@mariozechner/pi-tui": "0.52.12",
|
|
43
|
+
"@sinclair/typebox": "^0.34.48",
|
|
44
|
+
"@types/node": "^25.0.10",
|
|
45
|
+
"husky": "^9.1.7",
|
|
46
|
+
"typescript": "^5.9.3"
|
|
47
|
+
},
|
|
48
|
+
"pnpm": {
|
|
49
|
+
"overrides": {
|
|
50
|
+
"@mariozechner/pi-ai": "$@mariozechner/pi-coding-agent",
|
|
51
|
+
"@mariozechner/pi-tui": "$@mariozechner/pi-coding-agent"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
package/shell.nix
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* aperture:settings -- settings UI for Aperture configuration.
|
|
3
|
+
*
|
|
4
|
+
* Sections:
|
|
5
|
+
* - Connection: base URL
|
|
6
|
+
* - Providers: list of providers routed through Aperture
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
ArrayEditor,
|
|
11
|
+
registerSettingsCommand,
|
|
12
|
+
type SettingsSection,
|
|
13
|
+
setNestedValue,
|
|
14
|
+
} from "@aliou/pi-utils-settings";
|
|
15
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
16
|
+
import { getSettingsListTheme } from "@mariozechner/pi-coding-agent";
|
|
17
|
+
import type { ApertureConfig, ResolvedConfig } from "../config";
|
|
18
|
+
import { configLoader } from "../config";
|
|
19
|
+
|
|
20
|
+
export function registerApertureSettings(
|
|
21
|
+
pi: ExtensionAPI,
|
|
22
|
+
onConfigChange: () => void,
|
|
23
|
+
): void {
|
|
24
|
+
registerSettingsCommand<ApertureConfig, ResolvedConfig>(pi, {
|
|
25
|
+
commandName: "aperture:settings",
|
|
26
|
+
title: "Aperture Settings",
|
|
27
|
+
configStore: configLoader,
|
|
28
|
+
buildSections: (
|
|
29
|
+
tabConfig: ApertureConfig | null,
|
|
30
|
+
resolved: ResolvedConfig,
|
|
31
|
+
{ setDraft },
|
|
32
|
+
): SettingsSection[] => {
|
|
33
|
+
const settingsTheme = getSettingsListTheme();
|
|
34
|
+
|
|
35
|
+
const providers = tabConfig?.providers ?? resolved.providers;
|
|
36
|
+
|
|
37
|
+
return [
|
|
38
|
+
{
|
|
39
|
+
label: "Connection",
|
|
40
|
+
items: [
|
|
41
|
+
{
|
|
42
|
+
id: "baseUrl",
|
|
43
|
+
label: "Base URL",
|
|
44
|
+
description:
|
|
45
|
+
"Aperture gateway URL on your tailnet (e.g. http://ai.pango-lin.ts.net)",
|
|
46
|
+
currentValue:
|
|
47
|
+
(tabConfig?.baseUrl ?? resolved.baseUrl) || "(not set)",
|
|
48
|
+
values: undefined,
|
|
49
|
+
submenu: undefined,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
label: "Providers",
|
|
55
|
+
items: [
|
|
56
|
+
{
|
|
57
|
+
id: "providers",
|
|
58
|
+
label: "Routed providers",
|
|
59
|
+
description: "LLM providers routed through Aperture",
|
|
60
|
+
currentValue: `${providers.length} provider(s)`,
|
|
61
|
+
submenu: (_val, submenuDone) => {
|
|
62
|
+
let latest = [...providers];
|
|
63
|
+
return new ArrayEditor({
|
|
64
|
+
label: "Providers",
|
|
65
|
+
items: [...providers],
|
|
66
|
+
theme: settingsTheme,
|
|
67
|
+
onSave: (items) => {
|
|
68
|
+
latest = items;
|
|
69
|
+
const updated = structuredClone(
|
|
70
|
+
tabConfig ?? {},
|
|
71
|
+
) as ApertureConfig;
|
|
72
|
+
setNestedValue(updated, "providers", items);
|
|
73
|
+
setDraft(updated);
|
|
74
|
+
},
|
|
75
|
+
onDone: () => submenuDone(`${latest.length} provider(s)`),
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
},
|
|
83
|
+
onSettingChange: (id, newValue, config) => {
|
|
84
|
+
const updated = structuredClone(config);
|
|
85
|
+
if (id === "baseUrl") {
|
|
86
|
+
updated.baseUrl = newValue;
|
|
87
|
+
} else {
|
|
88
|
+
setNestedValue(updated, id, newValue);
|
|
89
|
+
}
|
|
90
|
+
return updated;
|
|
91
|
+
},
|
|
92
|
+
onSave: () => {
|
|
93
|
+
onConfigChange();
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* aperture:setup -- interactive wizard for configuring Aperture.
|
|
3
|
+
*
|
|
4
|
+
* Steps:
|
|
5
|
+
* 1. Ask for Aperture base URL (Input)
|
|
6
|
+
* 2. Select providers to route through Aperture (FuzzySelector, multi-select loop)
|
|
7
|
+
* 3. Save config and register providers
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { FuzzySelector } from "@aliou/pi-utils-settings";
|
|
11
|
+
import { getProviders } from "@mariozechner/pi-ai";
|
|
12
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
13
|
+
import { getSettingsListTheme } from "@mariozechner/pi-coding-agent";
|
|
14
|
+
import type { Component } from "@mariozechner/pi-tui";
|
|
15
|
+
import { Input, Key, matchesKey } from "@mariozechner/pi-tui";
|
|
16
|
+
import { configLoader } from "../config";
|
|
17
|
+
|
|
18
|
+
function normalizeUrl(url: string): string {
|
|
19
|
+
let result = url.trim();
|
|
20
|
+
if (!result) return result;
|
|
21
|
+
if (!result.startsWith("http://") && !result.startsWith("https://")) {
|
|
22
|
+
result = `http://${result}`;
|
|
23
|
+
}
|
|
24
|
+
return result.replace(/\/v1\/?$/, "").replace(/\/+$/, "");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Simple input prompt component for the base URL step.
|
|
29
|
+
*/
|
|
30
|
+
class UrlPrompt implements Component {
|
|
31
|
+
private input: Input;
|
|
32
|
+
private done: (value: string | undefined) => void;
|
|
33
|
+
private theme: ReturnType<typeof getSettingsListTheme>;
|
|
34
|
+
private placeholder = "ai.pango-lin.ts.net";
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
theme: ReturnType<typeof getSettingsListTheme>,
|
|
38
|
+
currentValue: string,
|
|
39
|
+
done: (value: string | undefined) => void,
|
|
40
|
+
) {
|
|
41
|
+
this.theme = theme;
|
|
42
|
+
this.done = done;
|
|
43
|
+
this.input = new Input();
|
|
44
|
+
if (currentValue) {
|
|
45
|
+
this.input.setValue(currentValue);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.input.onSubmit = () => {
|
|
49
|
+
const value = this.input.getValue().trim();
|
|
50
|
+
if (!value) return;
|
|
51
|
+
this.done(normalizeUrl(value));
|
|
52
|
+
};
|
|
53
|
+
this.input.onEscape = () => {
|
|
54
|
+
this.done(undefined);
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
render(width: number): string[] {
|
|
59
|
+
const lines: string[] = [];
|
|
60
|
+
lines.push(this.theme.label(" Aperture Setup", true));
|
|
61
|
+
lines.push("");
|
|
62
|
+
lines.push(
|
|
63
|
+
this.theme.hint(` Aperture base URL (e.g. ${this.placeholder}):`),
|
|
64
|
+
);
|
|
65
|
+
lines.push(` ${this.input.render(width - 4).join("")}`);
|
|
66
|
+
lines.push("");
|
|
67
|
+
lines.push(this.theme.hint(" Enter: confirm · Esc: cancel"));
|
|
68
|
+
return lines;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
invalidate() {}
|
|
72
|
+
|
|
73
|
+
handleInput(data: string) {
|
|
74
|
+
this.input.handleInput(data);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Provider multi-select component. Uses FuzzySelector for search,
|
|
80
|
+
* tracks selected providers, and lets the user confirm with Ctrl+S.
|
|
81
|
+
*/
|
|
82
|
+
class ProviderMultiSelect implements Component {
|
|
83
|
+
private allProviders: string[];
|
|
84
|
+
private selected: Set<string>;
|
|
85
|
+
private theme: ReturnType<typeof getSettingsListTheme>;
|
|
86
|
+
private done: (value: string[] | undefined) => void;
|
|
87
|
+
private fuzzy: FuzzySelector;
|
|
88
|
+
|
|
89
|
+
constructor(
|
|
90
|
+
theme: ReturnType<typeof getSettingsListTheme>,
|
|
91
|
+
providers: string[],
|
|
92
|
+
preselected: string[],
|
|
93
|
+
done: (value: string[] | undefined) => void,
|
|
94
|
+
) {
|
|
95
|
+
this.allProviders = providers;
|
|
96
|
+
this.selected = new Set(preselected);
|
|
97
|
+
this.theme = theme;
|
|
98
|
+
this.done = done;
|
|
99
|
+
|
|
100
|
+
this.fuzzy = new FuzzySelector({
|
|
101
|
+
label: "Select providers (Enter to toggle, Ctrl+S to confirm)",
|
|
102
|
+
items: this.allProviders,
|
|
103
|
+
theme,
|
|
104
|
+
onSelect: (value) => {
|
|
105
|
+
if (this.selected.has(value)) {
|
|
106
|
+
this.selected.delete(value);
|
|
107
|
+
} else {
|
|
108
|
+
this.selected.add(value);
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
onDone: () => {
|
|
112
|
+
this.done(undefined);
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
render(width: number): string[] {
|
|
118
|
+
const lines = this.fuzzy.render(width);
|
|
119
|
+
|
|
120
|
+
// Append selected providers summary
|
|
121
|
+
if (this.selected.size > 0) {
|
|
122
|
+
lines.push("");
|
|
123
|
+
lines.push(this.theme.hint(` Selected (${this.selected.size}):`));
|
|
124
|
+
for (const p of this.selected) {
|
|
125
|
+
lines.push(` ${this.theme.value(p, false)}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Replace the hint line from FuzzySelector
|
|
130
|
+
const hintIndex = lines.findIndex((l) => l.includes("Type to search"));
|
|
131
|
+
if (hintIndex !== -1) {
|
|
132
|
+
lines[hintIndex] = this.theme.hint(
|
|
133
|
+
" Type to search · Enter: toggle · Ctrl+S: confirm · Esc: cancel",
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return lines;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
invalidate() {
|
|
141
|
+
this.fuzzy.invalidate();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
handleInput(data: string) {
|
|
145
|
+
if (matchesKey(data, Key.ctrl("s"))) {
|
|
146
|
+
this.done([...this.selected]);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
this.fuzzy.handleInput(data);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function registerSetupCommand(
|
|
154
|
+
pi: ExtensionAPI,
|
|
155
|
+
onConfigChange: () => void,
|
|
156
|
+
): void {
|
|
157
|
+
pi.registerCommand("aperture:setup", {
|
|
158
|
+
description: "Configure Tailscale Aperture integration",
|
|
159
|
+
handler: async (_args, ctx) => {
|
|
160
|
+
if (!ctx.hasUI) {
|
|
161
|
+
ctx.ui.notify(
|
|
162
|
+
"aperture:setup requires an interactive terminal",
|
|
163
|
+
"error",
|
|
164
|
+
);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const config = configLoader.getConfig();
|
|
169
|
+
const settingsTheme = getSettingsListTheme();
|
|
170
|
+
|
|
171
|
+
// Step 1: base URL
|
|
172
|
+
const baseUrl = await ctx.ui.custom<string | undefined>(
|
|
173
|
+
(_tui, _theme, _kb, done) => {
|
|
174
|
+
return new UrlPrompt(settingsTheme, config.baseUrl, done);
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
if (!baseUrl) return;
|
|
179
|
+
|
|
180
|
+
// Step 2: select providers
|
|
181
|
+
const knownProviders = getProviders();
|
|
182
|
+
const providers = await ctx.ui.custom<string[] | undefined>(
|
|
183
|
+
(_tui, _theme, _kb, done) => {
|
|
184
|
+
return new ProviderMultiSelect(
|
|
185
|
+
settingsTheme,
|
|
186
|
+
knownProviders,
|
|
187
|
+
config.providers,
|
|
188
|
+
done,
|
|
189
|
+
);
|
|
190
|
+
},
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
if (!providers) return;
|
|
194
|
+
|
|
195
|
+
// Step 3: save and register
|
|
196
|
+
await configLoader.save("global", { baseUrl, providers });
|
|
197
|
+
onConfigChange();
|
|
198
|
+
ctx.ui.notify(
|
|
199
|
+
`Aperture configured: ${providers.length} provider(s) via ${baseUrl}`,
|
|
200
|
+
"info",
|
|
201
|
+
);
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration schema and loader for the Aperture extension.
|
|
3
|
+
*
|
|
4
|
+
* ApertureConfig is the user-facing schema (all fields optional).
|
|
5
|
+
* ResolvedConfig is the internal schema (all fields required, defaults applied).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ConfigLoader } from "@aliou/pi-utils-settings";
|
|
9
|
+
|
|
10
|
+
export interface ApertureConfig {
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
providers?: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ResolvedConfig {
|
|
16
|
+
baseUrl: string;
|
|
17
|
+
providers: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const DEFAULT_CONFIG: ResolvedConfig = {
|
|
21
|
+
baseUrl: "",
|
|
22
|
+
providers: [],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const configLoader = new ConfigLoader<ApertureConfig, ResolvedConfig>(
|
|
26
|
+
"aperture",
|
|
27
|
+
DEFAULT_CONFIG,
|
|
28
|
+
{ scopes: ["global"] },
|
|
29
|
+
);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi extension for Tailscale Aperture integration.
|
|
3
|
+
*
|
|
4
|
+
* Routes selected LLM providers through an Aperture gateway on your tailnet.
|
|
5
|
+
* Aperture handles API key injection and request routing, so this extension
|
|
6
|
+
* overrides each provider's baseUrl and sets a dummy apiKey.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
10
|
+
import { registerApertureSettings } from "./commands/settings";
|
|
11
|
+
import { registerSetupCommand } from "./commands/setup";
|
|
12
|
+
import { configLoader } from "./config";
|
|
13
|
+
|
|
14
|
+
function registerProviders(pi: ExtensionAPI): void {
|
|
15
|
+
const config = configLoader.getConfig();
|
|
16
|
+
if (!config.baseUrl || config.providers.length === 0) return;
|
|
17
|
+
|
|
18
|
+
const baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
19
|
+
|
|
20
|
+
for (const provider of config.providers) {
|
|
21
|
+
pi.registerProvider(provider, {
|
|
22
|
+
baseUrl: `${baseUrl}/v1`,
|
|
23
|
+
apiKey: "-",
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default async function (pi: ExtensionAPI): Promise<void> {
|
|
29
|
+
await configLoader.load();
|
|
30
|
+
|
|
31
|
+
const onConfigChange = () => {
|
|
32
|
+
// Config is already reloaded by configLoader.save(), just re-register.
|
|
33
|
+
registerProviders(pi);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
registerProviders(pi);
|
|
37
|
+
registerSetupCommand(pi, onConfigChange);
|
|
38
|
+
registerApertureSettings(pi, onConfigChange);
|
|
39
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"noEmit": true,
|
|
12
|
+
"jsx": "react-jsx",
|
|
13
|
+
"jsxImportSource": "@mariozechner/pi-tui"
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*"],
|
|
16
|
+
"exclude": ["node_modules"]
|
|
17
|
+
}
|