@plosson/agentio 0.8.3 → 0.8.5
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/package.json +1 -1
- package/src/commands/teleport.test.ts +196 -1
- package/src/commands/teleport.ts +168 -57
- package/src/server/favicon.ts +17 -0
- package/src/server/http.ts +11 -0
- package/src/server/setup-page.ts +2 -0
- package/src/types/config.ts +18 -0
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
type TeleportDeps,
|
|
13
13
|
} from './teleport';
|
|
14
14
|
import type { SiteioRunner, SiteioApp } from '../server/siteio-runner';
|
|
15
|
-
import type { Config } from '../types/config';
|
|
15
|
+
import type { Config, TeleportAppRecord } from '../types/config';
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Unit tests for `agentio teleport`. Every test injects a fake
|
|
@@ -41,6 +41,12 @@ interface FakeRunnerOptions {
|
|
|
41
41
|
deployedApp?: SiteioApp | null;
|
|
42
42
|
/** Stdout returned by logsApp. Default: empty string. */
|
|
43
43
|
logsStdout?: string;
|
|
44
|
+
/**
|
|
45
|
+
* If set, the first call to `deploy` throws an Error with this message;
|
|
46
|
+
* subsequent calls succeed. Used to simulate siteio rejecting `-f` on
|
|
47
|
+
* an app that was originally created with `-g` (git mode).
|
|
48
|
+
*/
|
|
49
|
+
failDeployFirstWith?: string;
|
|
44
50
|
failOn?:
|
|
45
51
|
| 'isInstalled'
|
|
46
52
|
| 'isLoggedIn'
|
|
@@ -88,6 +94,14 @@ function makeFakeRunner(opts: FakeRunnerOptions = {}): {
|
|
|
88
94
|
async deploy(args) {
|
|
89
95
|
calls.push({ method: 'deploy', args });
|
|
90
96
|
if (shouldFail('deploy')) throw new Error('deploy failed');
|
|
97
|
+
if (opts.failDeployFirstWith != null) {
|
|
98
|
+
const deployCallCount = calls.filter(
|
|
99
|
+
(c) => c.method === 'deploy'
|
|
100
|
+
).length;
|
|
101
|
+
if (deployCallCount === 1) {
|
|
102
|
+
throw new Error(opts.failDeployFirstWith);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
91
105
|
},
|
|
92
106
|
async restartApp(name) {
|
|
93
107
|
calls.push({ method: 'restartApp', args: { name } });
|
|
@@ -128,6 +142,11 @@ interface FakeDepsOptions extends FakeRunnerOptions {
|
|
|
128
142
|
* (a healthy 200). Pass [] to simulate an unreachable container.
|
|
129
143
|
*/
|
|
130
144
|
healthProbeResponses?: Array<number | null>;
|
|
145
|
+
/**
|
|
146
|
+
* The record `getLastTeleportApp` should return. Default: null (no
|
|
147
|
+
* app remembered yet).
|
|
148
|
+
*/
|
|
149
|
+
lastTeleportApp?: TeleportAppRecord | null;
|
|
131
150
|
}
|
|
132
151
|
|
|
133
152
|
interface FakeDeps extends TeleportDeps {
|
|
@@ -140,6 +159,8 @@ interface FakeDeps extends TeleportDeps {
|
|
|
140
159
|
tempFileDeletes: string[];
|
|
141
160
|
healthProbeUrls: string[];
|
|
142
161
|
sleepCalls: number[];
|
|
162
|
+
/** Records every call to `saveLastTeleportApp`. */
|
|
163
|
+
savedTeleportApps: TeleportAppRecord[];
|
|
143
164
|
}
|
|
144
165
|
|
|
145
166
|
function makeDeps(opts: FakeDepsOptions = {}): FakeDeps {
|
|
@@ -150,6 +171,7 @@ function makeDeps(opts: FakeDepsOptions = {}): FakeDeps {
|
|
|
150
171
|
const tempFileDeletes: string[] = [];
|
|
151
172
|
const healthProbeUrls: string[] = [];
|
|
152
173
|
const sleepCalls: number[] = [];
|
|
174
|
+
const savedTeleportApps: TeleportAppRecord[] = [];
|
|
153
175
|
|
|
154
176
|
let tempCounter = 0;
|
|
155
177
|
let healthProbeIdx = 0;
|
|
@@ -162,6 +184,7 @@ function makeDeps(opts: FakeDepsOptions = {}): FakeDeps {
|
|
|
162
184
|
tempFileDeletes,
|
|
163
185
|
healthProbeUrls,
|
|
164
186
|
sleepCalls,
|
|
187
|
+
savedTeleportApps,
|
|
165
188
|
runner,
|
|
166
189
|
loadConfig: async () =>
|
|
167
190
|
({
|
|
@@ -205,6 +228,11 @@ function makeDeps(opts: FakeDepsOptions = {}): FakeDeps {
|
|
|
205
228
|
sleep: async (ms) => {
|
|
206
229
|
sleepCalls.push(ms);
|
|
207
230
|
},
|
|
231
|
+
getLastTeleportApp: async () =>
|
|
232
|
+
'lastTeleportApp' in opts ? (opts.lastTeleportApp ?? null) : null,
|
|
233
|
+
saveLastTeleportApp: async (record) => {
|
|
234
|
+
savedTeleportApps.push(record);
|
|
235
|
+
},
|
|
208
236
|
log: (msg) => logLines.push(msg),
|
|
209
237
|
warn: (msg) => warnLines.push(msg),
|
|
210
238
|
};
|
|
@@ -433,6 +461,29 @@ describe('runTeleport — preflight failures', () => {
|
|
|
433
461
|
// already have their bearer.
|
|
434
462
|
expect(logs).not.toContain('To add to Claude Code:');
|
|
435
463
|
});
|
|
464
|
+
|
|
465
|
+
test('rebuild: deploy retries without -f if existing app was created in git mode', async () => {
|
|
466
|
+
const deps = makeDeps({
|
|
467
|
+
existingApp: { name: 'mcp', url: 'https://mcp.existing.com' },
|
|
468
|
+
failDeployFirstWith:
|
|
469
|
+
'siteio apps deploy mcp -f /tmp/Dockerfile failed (deploy mcp): exit 1\nx Cannot override Dockerfile: app was not created with -f',
|
|
470
|
+
});
|
|
471
|
+
const result = await runTeleport({ name: 'mcp' }, deps);
|
|
472
|
+
|
|
473
|
+
const deployCalls = deps.calls.filter((c) => c.method === 'deploy');
|
|
474
|
+
expect(deployCalls).toHaveLength(2);
|
|
475
|
+
// First attempt passed -f (inline Dockerfile)…
|
|
476
|
+
expect(
|
|
477
|
+
(deployCalls[0]!.args as { dockerfilePath?: string }).dockerfilePath
|
|
478
|
+
).toMatch(/Dockerfile$/);
|
|
479
|
+
// …second attempt omits -f so siteio uses its stored git settings.
|
|
480
|
+
expect(
|
|
481
|
+
(deployCalls[1]!.args as { dockerfilePath?: string }).dockerfilePath
|
|
482
|
+
).toBeUndefined();
|
|
483
|
+
|
|
484
|
+
expect(result.serverApiKey).toBe('');
|
|
485
|
+
expect(deps.logLines.join('\n')).toMatch(/not created with -f/i);
|
|
486
|
+
});
|
|
436
487
|
});
|
|
437
488
|
|
|
438
489
|
/* ------------------------------------------------------------------ */
|
|
@@ -1483,3 +1534,147 @@ describe('runTeleport — health check on --sync', () => {
|
|
|
1483
1534
|
expect(deps.warnLines.join('\n')).toContain('boom');
|
|
1484
1535
|
});
|
|
1485
1536
|
});
|
|
1537
|
+
|
|
1538
|
+
/* ------------------------------------------------------------------ */
|
|
1539
|
+
/* runTeleport — remembered app name */
|
|
1540
|
+
/* ------------------------------------------------------------------ */
|
|
1541
|
+
|
|
1542
|
+
describe('runTeleport — remembered app name', () => {
|
|
1543
|
+
test('full teleport saves lastApp on success', async () => {
|
|
1544
|
+
const deps = makeDeps();
|
|
1545
|
+
const before = Date.now();
|
|
1546
|
+
await runTeleport({ name: 'mcp' }, deps);
|
|
1547
|
+
const after = Date.now();
|
|
1548
|
+
|
|
1549
|
+
expect(deps.savedTeleportApps).toHaveLength(1);
|
|
1550
|
+
const saved = deps.savedTeleportApps[0]!;
|
|
1551
|
+
expect(saved.name).toBe('mcp');
|
|
1552
|
+
expect(saved.url).toBe('https://mcp.siteio.example.com');
|
|
1553
|
+
expect(saved.deployedAt).toBeGreaterThanOrEqual(before);
|
|
1554
|
+
expect(saved.deployedAt).toBeLessThanOrEqual(after);
|
|
1555
|
+
});
|
|
1556
|
+
|
|
1557
|
+
test('sync saves lastApp on success', async () => {
|
|
1558
|
+
const deps = makeDeps({
|
|
1559
|
+
existingApp: { name: 'mcp', url: 'https://mcp.x.com' },
|
|
1560
|
+
deployedApp: {
|
|
1561
|
+
name: 'mcp',
|
|
1562
|
+
url: 'https://mcp.x.com',
|
|
1563
|
+
volumes: [{ name: 'agentio-data-mcp', mountPath: '/data' }],
|
|
1564
|
+
},
|
|
1565
|
+
});
|
|
1566
|
+
await runTeleport({ name: 'mcp', sync: true }, deps);
|
|
1567
|
+
|
|
1568
|
+
expect(deps.savedTeleportApps).toHaveLength(1);
|
|
1569
|
+
expect(deps.savedTeleportApps[0]!.name).toBe('mcp');
|
|
1570
|
+
expect(deps.savedTeleportApps[0]!.url).toBe('https://mcp.x.com');
|
|
1571
|
+
});
|
|
1572
|
+
|
|
1573
|
+
test('sync without name uses remembered app', async () => {
|
|
1574
|
+
const deps = makeDeps({
|
|
1575
|
+
existingApp: { name: 'mcp', url: 'https://mcp.x.com' },
|
|
1576
|
+
deployedApp: {
|
|
1577
|
+
name: 'mcp',
|
|
1578
|
+
url: 'https://mcp.x.com',
|
|
1579
|
+
volumes: [{ name: 'agentio-data-mcp', mountPath: '/data' }],
|
|
1580
|
+
},
|
|
1581
|
+
lastTeleportApp: {
|
|
1582
|
+
name: 'mcp',
|
|
1583
|
+
url: 'https://mcp.x.com',
|
|
1584
|
+
deployedAt: 1_000_000,
|
|
1585
|
+
},
|
|
1586
|
+
});
|
|
1587
|
+
const result = await runTeleport({ sync: true }, deps);
|
|
1588
|
+
expect(result.name).toBe('mcp');
|
|
1589
|
+
// Confirm it actually reached the siteio calls with the remembered name.
|
|
1590
|
+
const findCall = deps.calls.find((c) => c.method === 'findApp');
|
|
1591
|
+
expect(findCall!.args).toEqual({ name: 'mcp' });
|
|
1592
|
+
// And announced the fallback to the user.
|
|
1593
|
+
expect(deps.logLines.join('\n')).toContain(
|
|
1594
|
+
'Using remembered teleport app "mcp"'
|
|
1595
|
+
);
|
|
1596
|
+
});
|
|
1597
|
+
|
|
1598
|
+
test('sync without name errors if nothing remembered', async () => {
|
|
1599
|
+
const deps = makeDeps({ lastTeleportApp: null });
|
|
1600
|
+
await expect(runTeleport({ sync: true }, deps)).rejects.toThrow(
|
|
1601
|
+
/no remembered teleport app/i
|
|
1602
|
+
);
|
|
1603
|
+
// Did not reach siteio.
|
|
1604
|
+
expect(deps.calls.map((c) => c.method)).not.toContain('findApp');
|
|
1605
|
+
});
|
|
1606
|
+
|
|
1607
|
+
test('full teleport without name uses remembered app', async () => {
|
|
1608
|
+
const deps = makeDeps({
|
|
1609
|
+
lastTeleportApp: {
|
|
1610
|
+
name: 'mcp',
|
|
1611
|
+
url: 'https://mcp.x.com',
|
|
1612
|
+
deployedAt: 1_000_000,
|
|
1613
|
+
},
|
|
1614
|
+
});
|
|
1615
|
+
const result = await runTeleport({}, deps);
|
|
1616
|
+
expect(result.name).toBe('mcp');
|
|
1617
|
+
const createCall = deps.calls.find((c) => c.method === 'createApp');
|
|
1618
|
+
// First time on this fresh runner, no existing app — falls through
|
|
1619
|
+
// to create. The create call must use the remembered name.
|
|
1620
|
+
expect((createCall!.args as { name: string }).name).toBe('mcp');
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
test('explicit name overrides remembered app', async () => {
|
|
1624
|
+
const deps = makeDeps({
|
|
1625
|
+
lastTeleportApp: {
|
|
1626
|
+
name: 'old-app',
|
|
1627
|
+
url: 'https://old.x.com',
|
|
1628
|
+
deployedAt: 1_000_000,
|
|
1629
|
+
},
|
|
1630
|
+
});
|
|
1631
|
+
await runTeleport({ name: 'new-app' }, deps);
|
|
1632
|
+
expect(deps.savedTeleportApps).toHaveLength(1);
|
|
1633
|
+
expect(deps.savedTeleportApps[0]!.name).toBe('new-app');
|
|
1634
|
+
const createCall = deps.calls.find((c) => c.method === 'createApp');
|
|
1635
|
+
expect((createCall!.args as { name: string }).name).toBe('new-app');
|
|
1636
|
+
});
|
|
1637
|
+
|
|
1638
|
+
test('dry-run does NOT save lastApp', async () => {
|
|
1639
|
+
const deps = makeDeps();
|
|
1640
|
+
await runTeleport({ name: 'mcp', dryRun: true }, deps);
|
|
1641
|
+
expect(deps.savedTeleportApps).toHaveLength(0);
|
|
1642
|
+
});
|
|
1643
|
+
|
|
1644
|
+
test('sync dry-run does NOT save lastApp', async () => {
|
|
1645
|
+
const deps = makeDeps({ existingApp: { name: 'mcp' } });
|
|
1646
|
+
await runTeleport(
|
|
1647
|
+
{ name: 'mcp', sync: true, dryRun: true },
|
|
1648
|
+
deps
|
|
1649
|
+
);
|
|
1650
|
+
expect(deps.savedTeleportApps).toHaveLength(0);
|
|
1651
|
+
});
|
|
1652
|
+
|
|
1653
|
+
test('failed full teleport (bad health) does NOT save lastApp', async () => {
|
|
1654
|
+
const deps = makeDeps({
|
|
1655
|
+
healthProbeResponses: [],
|
|
1656
|
+
logsStdout: 'crash\n',
|
|
1657
|
+
});
|
|
1658
|
+
await expect(runTeleport({ name: 'mcp' }, deps)).rejects.toThrow(
|
|
1659
|
+
/\/health never returned 200/
|
|
1660
|
+
);
|
|
1661
|
+
expect(deps.savedTeleportApps).toHaveLength(0);
|
|
1662
|
+
});
|
|
1663
|
+
|
|
1664
|
+
test('failed sync (bad health) does NOT save lastApp', async () => {
|
|
1665
|
+
const deps = makeDeps({
|
|
1666
|
+
existingApp: { name: 'mcp', url: 'https://mcp.x.com' },
|
|
1667
|
+
deployedApp: {
|
|
1668
|
+
name: 'mcp',
|
|
1669
|
+
url: 'https://mcp.x.com',
|
|
1670
|
+
volumes: [{ name: 'agentio-data-mcp', mountPath: '/data' }],
|
|
1671
|
+
},
|
|
1672
|
+
healthProbeResponses: [],
|
|
1673
|
+
logsStdout: 'crash\n',
|
|
1674
|
+
});
|
|
1675
|
+
await expect(
|
|
1676
|
+
runTeleport({ name: 'mcp', sync: true }, deps)
|
|
1677
|
+
).rejects.toThrow(/\/health never returned 200/);
|
|
1678
|
+
expect(deps.savedTeleportApps).toHaveLength(0);
|
|
1679
|
+
});
|
|
1680
|
+
});
|
package/src/commands/teleport.ts
CHANGED
|
@@ -5,14 +5,14 @@ import { join } from 'path';
|
|
|
5
5
|
import { tmpdir } from 'os';
|
|
6
6
|
|
|
7
7
|
import { generateExportData } from './config';
|
|
8
|
-
import { loadConfig } from '../config/config-manager';
|
|
8
|
+
import { loadConfig, saveConfig } from '../config/config-manager';
|
|
9
9
|
import { generateTeleportDockerfile } from '../server/dockerfile-gen';
|
|
10
10
|
import {
|
|
11
11
|
createSiteioRunner,
|
|
12
12
|
type SiteioRunner,
|
|
13
13
|
} from '../server/siteio-runner';
|
|
14
14
|
import { handleError, CliError } from '../utils/errors';
|
|
15
|
-
import type { Config } from '../types/config';
|
|
15
|
+
import type { Config, TeleportAppRecord } from '../types/config';
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* `agentio mcp teleport <name>` — one-command deploy of the local agentio
|
|
@@ -141,6 +141,17 @@ export interface TeleportDeps {
|
|
|
141
141
|
probeHealth: (url: string) => Promise<number | null>;
|
|
142
142
|
/** Resolved after `ms` milliseconds. Injected for testability. */
|
|
143
143
|
sleep: (ms: number) => Promise<void>;
|
|
144
|
+
/**
|
|
145
|
+
* Load the most recently teleported app. Used to resolve an
|
|
146
|
+
* omitted `<name>` argument (e.g. `agentio mcp teleport --sync`).
|
|
147
|
+
* Returns null if nothing has been remembered yet.
|
|
148
|
+
*/
|
|
149
|
+
getLastTeleportApp: () => Promise<TeleportAppRecord | null>;
|
|
150
|
+
/**
|
|
151
|
+
* Persist the most recently teleported app so future invocations
|
|
152
|
+
* without a `<name>` can default to it.
|
|
153
|
+
*/
|
|
154
|
+
saveLastTeleportApp: (record: TeleportAppRecord) => Promise<void>;
|
|
144
155
|
log: (msg: string) => void;
|
|
145
156
|
warn: (msg: string) => void;
|
|
146
157
|
}
|
|
@@ -187,7 +198,12 @@ export async function waitForHealth(
|
|
|
187
198
|
}
|
|
188
199
|
|
|
189
200
|
export interface TeleportOptions {
|
|
190
|
-
|
|
201
|
+
/**
|
|
202
|
+
* Siteio app name. Optional: if omitted, `runTeleport` will fall
|
|
203
|
+
* back to the most recently teleported app recorded in config
|
|
204
|
+
* (`config.teleport.lastApp`). Required for the very first deploy.
|
|
205
|
+
*/
|
|
206
|
+
name?: string;
|
|
191
207
|
dockerfileOnly?: boolean;
|
|
192
208
|
output?: string;
|
|
193
209
|
dryRun?: boolean;
|
|
@@ -227,6 +243,32 @@ export interface TeleportResult {
|
|
|
227
243
|
claudeMcpAddCommand: string | null;
|
|
228
244
|
}
|
|
229
245
|
|
|
246
|
+
/**
|
|
247
|
+
* Resolve the siteio app name, falling back to the remembered one if
|
|
248
|
+
* the caller omitted `<name>`. Validates the resulting name. Throws a
|
|
249
|
+
* user-facing CliError if nothing is provided and nothing is remembered.
|
|
250
|
+
*/
|
|
251
|
+
async function resolveAppName(
|
|
252
|
+
requested: string | undefined,
|
|
253
|
+
deps: TeleportDeps
|
|
254
|
+
): Promise<string> {
|
|
255
|
+
if (requested) {
|
|
256
|
+
validateAppName(requested);
|
|
257
|
+
return requested;
|
|
258
|
+
}
|
|
259
|
+
const last = await deps.getLastTeleportApp();
|
|
260
|
+
if (!last) {
|
|
261
|
+
throw new CliError(
|
|
262
|
+
'INVALID_PARAMS',
|
|
263
|
+
'No app name provided and no remembered teleport app',
|
|
264
|
+
'Pass <name> explicitly the first time, e.g. `agentio mcp teleport mcp`. The name is remembered after the first successful deploy.'
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
validateAppName(last.name);
|
|
268
|
+
deps.log(`Using remembered teleport app "${last.name}".`);
|
|
269
|
+
return last.name;
|
|
270
|
+
}
|
|
271
|
+
|
|
230
272
|
/**
|
|
231
273
|
* Sync mode: re-export local config and push it to an existing siteio
|
|
232
274
|
* app, then restart. Same dependency-injection model as runTeleport
|
|
@@ -236,6 +278,7 @@ async function runSync(
|
|
|
236
278
|
opts: TeleportOptions,
|
|
237
279
|
deps: TeleportDeps
|
|
238
280
|
): Promise<TeleportResult> {
|
|
281
|
+
const name = await resolveAppName(opts.name, deps);
|
|
239
282
|
// Preflight: same as full teleport.
|
|
240
283
|
deps.log('Checking siteio…');
|
|
241
284
|
if (!(await deps.runner.isInstalled())) {
|
|
@@ -268,13 +311,13 @@ async function runSync(
|
|
|
268
311
|
|
|
269
312
|
// Sync requires the app to ALREADY EXIST. This is the inverse of the
|
|
270
313
|
// normal teleport check.
|
|
271
|
-
deps.log(`Checking that siteio app "${
|
|
272
|
-
const existing = await deps.runner.findApp(
|
|
314
|
+
deps.log(`Checking that siteio app "${name}" exists…`);
|
|
315
|
+
const existing = await deps.runner.findApp(name);
|
|
273
316
|
if (!existing) {
|
|
274
317
|
throw new CliError(
|
|
275
318
|
'NOT_FOUND',
|
|
276
|
-
`No siteio app named "${
|
|
277
|
-
`Run \`agentio mcp teleport ${
|
|
319
|
+
`No siteio app named "${name}" to sync to`,
|
|
320
|
+
`Run \`agentio mcp teleport ${name}\` (without --sync) first to create it.`
|
|
278
321
|
);
|
|
279
322
|
}
|
|
280
323
|
|
|
@@ -285,11 +328,11 @@ async function runSync(
|
|
|
285
328
|
// Detect whether /data is already mounted as a persistent volume.
|
|
286
329
|
// If not, attach it as part of this sync (one-time backfill for apps
|
|
287
330
|
// teleported before the volume was a default).
|
|
288
|
-
const detail = await deps.runner.appInfo(
|
|
331
|
+
const detail = await deps.runner.appInfo(name);
|
|
289
332
|
const needsVolumeBackfill = !hasDataVolumeMount(detail);
|
|
290
333
|
if (needsVolumeBackfill) {
|
|
291
334
|
deps.log(
|
|
292
|
-
`No persistent volume mounted at ${DATA_VOLUME_PATH} — will attach ${volumeNameFor(
|
|
335
|
+
`No persistent volume mounted at ${DATA_VOLUME_PATH} — will attach ${volumeNameFor(name)}:${DATA_VOLUME_PATH} as part of this sync.`
|
|
293
336
|
);
|
|
294
337
|
}
|
|
295
338
|
|
|
@@ -297,22 +340,22 @@ async function runSync(
|
|
|
297
340
|
if (opts.dryRun) {
|
|
298
341
|
deps.log('--- Dry run: the following commands would be executed ---');
|
|
299
342
|
const dryParts = [
|
|
300
|
-
`siteio apps set ${
|
|
343
|
+
`siteio apps set ${name}`,
|
|
301
344
|
'-e AGENTIO_KEY=<redacted>',
|
|
302
345
|
`-e AGENTIO_CONFIG=<${exported.config.length} chars>`,
|
|
303
346
|
];
|
|
304
347
|
if (needsVolumeBackfill) {
|
|
305
348
|
dryParts.push(
|
|
306
|
-
`-v ${volumeNameFor(
|
|
349
|
+
`-v ${volumeNameFor(name)}:${DATA_VOLUME_PATH}`
|
|
307
350
|
);
|
|
308
351
|
}
|
|
309
352
|
deps.log(dryParts.join(' '));
|
|
310
|
-
deps.log(`siteio apps restart ${
|
|
353
|
+
deps.log(`siteio apps restart ${name}`);
|
|
311
354
|
deps.log(
|
|
312
355
|
'(AGENTIO_SERVER_API_KEY is intentionally NOT touched — operator key on the remote stays the same.)'
|
|
313
356
|
);
|
|
314
357
|
return {
|
|
315
|
-
name:
|
|
358
|
+
name: name,
|
|
316
359
|
serverApiKey: '',
|
|
317
360
|
claudeMcpAddCommand: null,
|
|
318
361
|
};
|
|
@@ -333,20 +376,20 @@ async function runSync(
|
|
|
333
376
|
// REPLACES the volumes list on update (env merges; volumes don't),
|
|
334
377
|
// so attaching when something else is mounted would clobber it.
|
|
335
378
|
await deps.runner.setApp({
|
|
336
|
-
name:
|
|
379
|
+
name: name,
|
|
337
380
|
envVars: {
|
|
338
381
|
AGENTIO_KEY: exported.key,
|
|
339
382
|
AGENTIO_CONFIG: exported.config,
|
|
340
383
|
},
|
|
341
384
|
...(needsVolumeBackfill
|
|
342
385
|
? {
|
|
343
|
-
volumes: { [volumeNameFor(
|
|
386
|
+
volumes: { [volumeNameFor(name)]: DATA_VOLUME_PATH },
|
|
344
387
|
}
|
|
345
388
|
: {}),
|
|
346
389
|
});
|
|
347
390
|
|
|
348
391
|
deps.log('Restarting container so the new env vars take effect…');
|
|
349
|
-
await deps.runner.restartApp(
|
|
392
|
+
await deps.runner.restartApp(name);
|
|
350
393
|
|
|
351
394
|
// We already fetched appInfo earlier for volume detection; reuse
|
|
352
395
|
// its URL field rather than calling again. Same fallback as the
|
|
@@ -354,7 +397,7 @@ async function runSync(
|
|
|
354
397
|
// generated subdomain URL, so fall back to findApp if it's missing.
|
|
355
398
|
let url = typeof detail?.url === 'string' ? detail.url : undefined;
|
|
356
399
|
if (!url) {
|
|
357
|
-
const listed = await deps.runner.findApp(
|
|
400
|
+
const listed = await deps.runner.findApp(name);
|
|
358
401
|
if (typeof listed?.url === 'string') url = listed.url;
|
|
359
402
|
}
|
|
360
403
|
|
|
@@ -368,7 +411,7 @@ async function runSync(
|
|
|
368
411
|
deps.warn(
|
|
369
412
|
`Container failed to report healthy after ${Math.round(HEALTH_TIMEOUT_MS / 1000)}s. Fetching logs…`
|
|
370
413
|
);
|
|
371
|
-
const logs = await deps.runner.logsApp(
|
|
414
|
+
const logs = await deps.runner.logsApp(name, {
|
|
372
415
|
tail: HEALTH_FAILURE_LOG_TAIL,
|
|
373
416
|
});
|
|
374
417
|
deps.warn('--- container logs (tail) ---');
|
|
@@ -376,12 +419,21 @@ async function runSync(
|
|
|
376
419
|
deps.warn('--- end logs ---');
|
|
377
420
|
throw new CliError(
|
|
378
421
|
'API_ERROR',
|
|
379
|
-
`Sync to "${
|
|
422
|
+
`Sync to "${name}" restarted the container but /health never returned 200`,
|
|
380
423
|
'Inspect the logs above. The previous config is gone — the next sync (or a manual `siteio apps restart`) will still see the broken state until you fix the root cause.'
|
|
381
424
|
);
|
|
382
425
|
}
|
|
383
426
|
}
|
|
384
427
|
|
|
428
|
+
// Remember the app so a future bare `--sync` (or any name-less
|
|
429
|
+
// teleport invocation) can default to it. Only reached once the
|
|
430
|
+
// restart + health check have both succeeded.
|
|
431
|
+
await deps.saveLastTeleportApp({
|
|
432
|
+
name,
|
|
433
|
+
url,
|
|
434
|
+
deployedAt: Date.now(),
|
|
435
|
+
});
|
|
436
|
+
|
|
385
437
|
deps.log('');
|
|
386
438
|
deps.log('Sync complete!');
|
|
387
439
|
if (url) {
|
|
@@ -408,7 +460,7 @@ async function runSync(
|
|
|
408
460
|
}
|
|
409
461
|
|
|
410
462
|
return {
|
|
411
|
-
name:
|
|
463
|
+
name: name,
|
|
412
464
|
url,
|
|
413
465
|
// We did not generate a new server key in sync mode.
|
|
414
466
|
serverApiKey: '',
|
|
@@ -424,8 +476,6 @@ export async function runTeleport(
|
|
|
424
476
|
opts: TeleportOptions,
|
|
425
477
|
deps: TeleportDeps
|
|
426
478
|
): Promise<TeleportResult> {
|
|
427
|
-
validateAppName(opts.name);
|
|
428
|
-
|
|
429
479
|
// Sync mode short-circuits — different command shape, different
|
|
430
480
|
// preflight (app must EXIST, not absent), no Dockerfile work, no
|
|
431
481
|
// create. Mutual exclusion with the "create new app" flags.
|
|
@@ -461,6 +511,11 @@ export async function runTeleport(
|
|
|
461
511
|
return runSync(opts, deps);
|
|
462
512
|
}
|
|
463
513
|
|
|
514
|
+
// Resolve the app name up front so every downstream path (including
|
|
515
|
+
// dockerfile-only and dry-run) uses a single consistent value, and
|
|
516
|
+
// so an omitted name cleanly falls back to the remembered app.
|
|
517
|
+
const name = await resolveAppName(opts.name, deps);
|
|
518
|
+
|
|
464
519
|
// dockerfile-only: skip every siteio interaction, just emit the
|
|
465
520
|
// Dockerfile to stdout or a file and return.
|
|
466
521
|
if (opts.dockerfileOnly) {
|
|
@@ -476,7 +531,7 @@ export async function runTeleport(
|
|
|
476
531
|
process.stdout.write(content);
|
|
477
532
|
}
|
|
478
533
|
return {
|
|
479
|
-
name:
|
|
534
|
+
name: name,
|
|
480
535
|
serverApiKey: '',
|
|
481
536
|
claudeMcpAddCommand: null,
|
|
482
537
|
};
|
|
@@ -519,16 +574,16 @@ export async function runTeleport(
|
|
|
519
574
|
// and their issued bearers), backfill /data if needed, and redeploy
|
|
520
575
|
// the freshly generated image. If it doesn't exist, fall through to
|
|
521
576
|
// the fresh-deploy path.
|
|
522
|
-
deps.log(`Checking if siteio app "${
|
|
523
|
-
const existing = await deps.runner.findApp(
|
|
577
|
+
deps.log(`Checking if siteio app "${name}" already exists…`);
|
|
578
|
+
const existing = await deps.runner.findApp(name);
|
|
524
579
|
const isRebuild = Boolean(existing);
|
|
525
580
|
if (isRebuild) {
|
|
526
581
|
deps.log(
|
|
527
|
-
`Found existing siteio app "${
|
|
582
|
+
`Found existing siteio app "${name}" — will rebuild image in place (API key and clients preserved).`
|
|
528
583
|
);
|
|
529
584
|
} else {
|
|
530
585
|
deps.log(
|
|
531
|
-
`No existing siteio app "${
|
|
586
|
+
`No existing siteio app "${name}" — will create a fresh one.`
|
|
532
587
|
);
|
|
533
588
|
}
|
|
534
589
|
|
|
@@ -550,11 +605,11 @@ export async function runTeleport(
|
|
|
550
605
|
// the persistent volume if it's missing (same logic as --sync).
|
|
551
606
|
let needsVolumeBackfill = false;
|
|
552
607
|
if (isRebuild) {
|
|
553
|
-
const detail = await deps.runner.appInfo(
|
|
608
|
+
const detail = await deps.runner.appInfo(name);
|
|
554
609
|
needsVolumeBackfill = !hasDataVolumeMount(detail);
|
|
555
610
|
if (needsVolumeBackfill) {
|
|
556
611
|
deps.log(
|
|
557
|
-
`No persistent volume mounted at ${DATA_VOLUME_PATH} — will attach ${volumeNameFor(
|
|
612
|
+
`No persistent volume mounted at ${DATA_VOLUME_PATH} — will attach ${volumeNameFor(name)}:${DATA_VOLUME_PATH} as part of this rebuild.`
|
|
558
613
|
);
|
|
559
614
|
}
|
|
560
615
|
}
|
|
@@ -590,16 +645,16 @@ export async function runTeleport(
|
|
|
590
645
|
if (!isRebuild) {
|
|
591
646
|
if (gitSettings) {
|
|
592
647
|
deps.log(
|
|
593
|
-
`siteio apps create ${
|
|
648
|
+
`siteio apps create ${name} -g ${gitSettings.repoUrl} --branch ${gitSettings.branch} --dockerfile ${TELEPORT_DOCKERFILE_PATH} -p 9999`
|
|
594
649
|
);
|
|
595
650
|
} else {
|
|
596
651
|
deps.log(
|
|
597
|
-
`siteio apps create ${
|
|
652
|
+
`siteio apps create ${name} -f <tempfile> -p 9999`
|
|
598
653
|
);
|
|
599
654
|
}
|
|
600
655
|
}
|
|
601
656
|
const setParts = [
|
|
602
|
-
`siteio apps set ${
|
|
657
|
+
`siteio apps set ${name}`,
|
|
603
658
|
'-e AGENTIO_KEY=<redacted>',
|
|
604
659
|
`-e AGENTIO_CONFIG=<${exported.config.length} chars>`,
|
|
605
660
|
];
|
|
@@ -607,11 +662,11 @@ export async function runTeleport(
|
|
|
607
662
|
setParts.push(`-e AGENTIO_SERVER_API_KEY=${serverApiKey}`);
|
|
608
663
|
}
|
|
609
664
|
if (!isRebuild || needsVolumeBackfill) {
|
|
610
|
-
setParts.push(`-v ${volumeNameFor(
|
|
665
|
+
setParts.push(`-v ${volumeNameFor(name)}:${DATA_VOLUME_PATH}`);
|
|
611
666
|
}
|
|
612
667
|
deps.log(setParts.join(' '));
|
|
613
668
|
deps.log(
|
|
614
|
-
`siteio apps deploy ${
|
|
669
|
+
`siteio apps deploy ${name}${opts.noCache ? ' --no-cache' : ''}`
|
|
615
670
|
);
|
|
616
671
|
if (isRebuild) {
|
|
617
672
|
deps.log(
|
|
@@ -628,7 +683,7 @@ export async function runTeleport(
|
|
|
628
683
|
);
|
|
629
684
|
}
|
|
630
685
|
return {
|
|
631
|
-
name:
|
|
686
|
+
name: name,
|
|
632
687
|
serverApiKey,
|
|
633
688
|
claudeMcpAddCommand: null,
|
|
634
689
|
};
|
|
@@ -643,10 +698,10 @@ export async function runTeleport(
|
|
|
643
698
|
|
|
644
699
|
try {
|
|
645
700
|
if (!isRebuild) {
|
|
646
|
-
deps.log(`Creating siteio app "${
|
|
701
|
+
deps.log(`Creating siteio app "${name}"…`);
|
|
647
702
|
if (gitSettings) {
|
|
648
703
|
await deps.runner.createApp({
|
|
649
|
-
name:
|
|
704
|
+
name: name,
|
|
650
705
|
port: 9999,
|
|
651
706
|
git: {
|
|
652
707
|
repoUrl: gitSettings.repoUrl,
|
|
@@ -656,7 +711,7 @@ export async function runTeleport(
|
|
|
656
711
|
});
|
|
657
712
|
} else {
|
|
658
713
|
await deps.runner.createApp({
|
|
659
|
-
name:
|
|
714
|
+
name: name,
|
|
660
715
|
dockerfilePath: tempPath!,
|
|
661
716
|
port: 9999,
|
|
662
717
|
});
|
|
@@ -686,10 +741,10 @@ export async function runTeleport(
|
|
|
686
741
|
}
|
|
687
742
|
|
|
688
743
|
await deps.runner.setApp({
|
|
689
|
-
name:
|
|
744
|
+
name: name,
|
|
690
745
|
envVars,
|
|
691
746
|
...(attachVolume
|
|
692
|
-
? { volumes: { [volumeNameFor(
|
|
747
|
+
? { volumes: { [volumeNameFor(name)]: DATA_VOLUME_PATH } }
|
|
693
748
|
: {}),
|
|
694
749
|
});
|
|
695
750
|
|
|
@@ -698,17 +753,39 @@ export async function runTeleport(
|
|
|
698
753
|
? 'Rebuilding (this may take a minute — Docker is rebuilding your image)…'
|
|
699
754
|
: 'Deploying (this may take a minute — Docker is building your image)…'
|
|
700
755
|
);
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
756
|
+
try {
|
|
757
|
+
await deps.runner.deploy({
|
|
758
|
+
name: name,
|
|
759
|
+
// In git mode, there's no -f to re-pass on deploy — siteio uses
|
|
760
|
+
// the stored git settings from create.
|
|
761
|
+
...(tempPath ? { dockerfilePath: tempPath } : {}),
|
|
762
|
+
noCache: opts.noCache,
|
|
763
|
+
});
|
|
764
|
+
} catch (err) {
|
|
765
|
+
// The existing app may have been created in git mode (no -f). siteio
|
|
766
|
+
// refuses `deploy -f` in that case with a specific error — retry
|
|
767
|
+
// without -f so it falls back to the stored git settings.
|
|
768
|
+
if (
|
|
769
|
+
isRebuild &&
|
|
770
|
+
tempPath &&
|
|
771
|
+
err instanceof Error &&
|
|
772
|
+
/not created with -f/i.test(err.message)
|
|
773
|
+
) {
|
|
774
|
+
deps.log(
|
|
775
|
+
'Existing app was not created with -f — retrying deploy without inline Dockerfile (siteio will rebuild from its stored git settings).'
|
|
776
|
+
);
|
|
777
|
+
await deps.runner.deploy({
|
|
778
|
+
name: name,
|
|
779
|
+
noCache: opts.noCache,
|
|
780
|
+
});
|
|
781
|
+
} else {
|
|
782
|
+
throw err;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
708
785
|
|
|
709
786
|
// Try to surface the deployed URL. Non-fatal if siteio doesn't
|
|
710
787
|
// give us one back.
|
|
711
|
-
const info = await deps.runner.appInfo(
|
|
788
|
+
const info = await deps.runner.appInfo(name);
|
|
712
789
|
let url = typeof info?.url === 'string' ? info.url : undefined;
|
|
713
790
|
// siteio's `apps info --json` output omits the generated subdomain
|
|
714
791
|
// URL (domains: [] in the payload) even though the app is reachable
|
|
@@ -716,7 +793,7 @@ export async function runTeleport(
|
|
|
716
793
|
// level. Fall back to findApp so the post-deploy health check can
|
|
717
794
|
// still run even when siteio doesn't surface url in info.
|
|
718
795
|
if (!url) {
|
|
719
|
-
const listed = await deps.runner.findApp(
|
|
796
|
+
const listed = await deps.runner.findApp(name);
|
|
720
797
|
if (typeof listed?.url === 'string') url = listed.url;
|
|
721
798
|
}
|
|
722
799
|
|
|
@@ -731,7 +808,7 @@ export async function runTeleport(
|
|
|
731
808
|
deps.warn(
|
|
732
809
|
`Container failed to report healthy after ${Math.round(HEALTH_TIMEOUT_MS / 1000)}s. Fetching logs…`
|
|
733
810
|
);
|
|
734
|
-
const logs = await deps.runner.logsApp(
|
|
811
|
+
const logs = await deps.runner.logsApp(name, {
|
|
735
812
|
tail: HEALTH_FAILURE_LOG_TAIL,
|
|
736
813
|
});
|
|
737
814
|
deps.warn('--- container logs (tail) ---');
|
|
@@ -739,14 +816,14 @@ export async function runTeleport(
|
|
|
739
816
|
deps.warn('--- end logs ---');
|
|
740
817
|
throw new CliError(
|
|
741
818
|
'API_ERROR',
|
|
742
|
-
`Deploy "${
|
|
819
|
+
`Deploy "${name}" started but /health never returned 200`,
|
|
743
820
|
'Inspect the logs above. Common causes: permission errors on mounted volumes, missing env vars, binary not found for the container arch.'
|
|
744
821
|
);
|
|
745
822
|
}
|
|
746
823
|
} else {
|
|
747
824
|
deps.warn(
|
|
748
825
|
'Skipping health check: siteio did not return a URL for this app. ' +
|
|
749
|
-
`Run \`siteio apps info ${
|
|
826
|
+
`Run \`siteio apps info ${name}\` and curl <url>/health manually to verify.`
|
|
750
827
|
);
|
|
751
828
|
}
|
|
752
829
|
|
|
@@ -754,6 +831,15 @@ export async function runTeleport(
|
|
|
754
831
|
? `claude mcp add --scope local --transport http agentio "${url}/mcp?services=rss"`
|
|
755
832
|
: null;
|
|
756
833
|
|
|
834
|
+
// Remember the app so a future bare `--sync` (or any name-less
|
|
835
|
+
// teleport invocation) can default to it. Only reached once the
|
|
836
|
+
// deploy + health check have both succeeded.
|
|
837
|
+
await deps.saveLastTeleportApp({
|
|
838
|
+
name,
|
|
839
|
+
url,
|
|
840
|
+
deployedAt: Date.now(),
|
|
841
|
+
});
|
|
842
|
+
|
|
757
843
|
deps.log('');
|
|
758
844
|
deps.log(isRebuild ? 'Rebuild complete!' : 'Teleport complete!');
|
|
759
845
|
if (url) {
|
|
@@ -762,7 +848,7 @@ export async function runTeleport(
|
|
|
762
848
|
deps.log(` MCP: ${url}/mcp`);
|
|
763
849
|
} else {
|
|
764
850
|
deps.log(
|
|
765
|
-
` URL: (siteio did not return a URL — run \`siteio apps info ${
|
|
851
|
+
` URL: (siteio did not return a URL — run \`siteio apps info ${name}\` to look it up)`
|
|
766
852
|
);
|
|
767
853
|
}
|
|
768
854
|
if (isRebuild) {
|
|
@@ -781,7 +867,7 @@ export async function runTeleport(
|
|
|
781
867
|
}
|
|
782
868
|
|
|
783
869
|
return {
|
|
784
|
-
name:
|
|
870
|
+
name: name,
|
|
785
871
|
url,
|
|
786
872
|
serverApiKey,
|
|
787
873
|
claudeMcpAddCommand: claudeCmd,
|
|
@@ -847,6 +933,29 @@ async function defaultSleep(ms: number): Promise<void> {
|
|
|
847
933
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
848
934
|
}
|
|
849
935
|
|
|
936
|
+
/**
|
|
937
|
+
* Default `getLastTeleportApp`: read `config.teleport.lastApp` from
|
|
938
|
+
* `~/.config/agentio/config.json`. Returns null if the config has no
|
|
939
|
+
* teleport memory yet.
|
|
940
|
+
*/
|
|
941
|
+
async function defaultGetLastTeleportApp(): Promise<TeleportAppRecord | null> {
|
|
942
|
+
const config = (await loadConfig()) as Config;
|
|
943
|
+
return config.teleport?.lastApp ?? null;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* Default `saveLastTeleportApp`: merge into `config.teleport.lastApp`
|
|
948
|
+
* and persist. Single-slot — the most recent successful teleport
|
|
949
|
+
* overwrites whatever was there before.
|
|
950
|
+
*/
|
|
951
|
+
async function defaultSaveLastTeleportApp(
|
|
952
|
+
record: TeleportAppRecord
|
|
953
|
+
): Promise<void> {
|
|
954
|
+
const config = (await loadConfig()) as Config;
|
|
955
|
+
config.teleport = { ...(config.teleport ?? {}), lastApp: record };
|
|
956
|
+
await saveConfig(config);
|
|
957
|
+
}
|
|
958
|
+
|
|
850
959
|
async function defaultDetectGitOriginUrl(): Promise<string | null> {
|
|
851
960
|
try {
|
|
852
961
|
const proc = Bun.spawn(['git', 'remote', 'get-url', 'origin'], {
|
|
@@ -877,8 +986,8 @@ export function registerTeleportCommand(parent: Command): void {
|
|
|
877
986
|
'Deploy the agentio HTTP MCP server to a siteio-managed remote in one command'
|
|
878
987
|
)
|
|
879
988
|
.argument(
|
|
880
|
-
'
|
|
881
|
-
'Siteio app name (becomes the subdomain: e.g. "mcp" → mcp.<your-siteio-domain>)'
|
|
989
|
+
'[name]',
|
|
990
|
+
'Siteio app name (becomes the subdomain: e.g. "mcp" → mcp.<your-siteio-domain>). Optional on subsequent runs — defaults to the most recently deployed app.'
|
|
882
991
|
)
|
|
883
992
|
.option(
|
|
884
993
|
'--dockerfile-only',
|
|
@@ -908,7 +1017,7 @@ export function registerTeleportCommand(parent: Command): void {
|
|
|
908
1017
|
'--sync',
|
|
909
1018
|
'Push the latest local config (profiles + credentials) to an EXISTING siteio app and restart it. Use after adding/changing a profile. Does not rebuild the image; does not change the operator API key.'
|
|
910
1019
|
)
|
|
911
|
-
.action(async (name: string, options) => {
|
|
1020
|
+
.action(async (name: string | undefined, options) => {
|
|
912
1021
|
try {
|
|
913
1022
|
const runner = createSiteioRunner();
|
|
914
1023
|
await runTeleport(
|
|
@@ -937,6 +1046,8 @@ export function registerTeleportCommand(parent: Command): void {
|
|
|
937
1046
|
detectGitOriginUrl: defaultDetectGitOriginUrl,
|
|
938
1047
|
probeHealth: defaultProbeHealth,
|
|
939
1048
|
sleep: defaultSleep,
|
|
1049
|
+
getLastTeleportApp: defaultGetLastTeleportApp,
|
|
1050
|
+
saveLastTeleportApp: defaultSaveLastTeleportApp,
|
|
940
1051
|
log: (msg) => console.log(msg),
|
|
941
1052
|
warn: (msg) => console.error(msg),
|
|
942
1053
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Embedded favicon served at `/favicon.ico` and `/favicon.png`. Keeps MCP
|
|
3
|
+
* clients (e.g. Claude's connector UI) from falling back to the generic
|
|
4
|
+
* globe icon. Bytes are inlined so every build target — dev, `bun build
|
|
5
|
+
* --target node`, and `bun build --compile` — ships it without filesystem
|
|
6
|
+
* lookups.
|
|
7
|
+
*
|
|
8
|
+
* Source: `site/logo.png` (120x120 PNG).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const FAVICON_BASE64 =
|
|
12
|
+
'iVBORw0KGgoAAAANSUhEUgAAAHgAAAB4EAIAAADmln3GAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAAGYktHRP///////wlY99wAAAAHdElNRQfqAQMQAyhWYExTAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDI2LTAxLTAzVDE2OjAzOjM5KzAwOjAwTY3PQAAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyNi0wMS0wM1QxNjowMzozOSswMDowMDzQd/wAAAAodEVYdGRhdGU6dGltZXN0YW1wADIwMjYtMDEtMDNUMTY6MDM6MzkrMDA6MDBrxVYjAAAMjklEQVR42u3dd1QU1x4H8Dtll04EREBFEBSVIiWCFAUBsccSTYyFGKOIPp9HeXmWYIklqKioiSHRaKIGsSTHEiNRH6iRpgRDsUSKiGIE9IkNZZfdnZn3x7z3Ts5LZnbdyt73+/zlcX87d+71e2an3DsScXFpaS9eIACwQJp6BwDQJwg0wAoEGmAFAg2wAoEGWIFAA6xAoAFWINAAKxBogBUINMAKBBpgBQINsAKBBliBQAOsQKABViDQACsQaIAVCDTACgQaYAUCDbACgQZYgUADrECgAVYg0AArEGiAFQg0wAoEGmAFAg2wAoEGWIFAA6xAoAFWINAAKxBogBUINMAKBBpgBQINsAKBBliBQAOsQKABViDQACsQaIAVCDTACgQaYAUCDbACgQZYgUADrECgAVYg0AArEGiAFQg0wAoEGmAFAg2w8i9H6natiosOZAAAAABJRU5ErkJggg==';
|
|
13
|
+
|
|
14
|
+
export const FAVICON_BYTES = Uint8Array.from(
|
|
15
|
+
Buffer.from(FAVICON_BASE64, 'base64')
|
|
16
|
+
);
|
|
17
|
+
export const FAVICON_CONTENT_TYPE = 'image/png';
|
package/src/server/http.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { handleMcpRequest } from './mcp-http';
|
|
|
2
2
|
import type { OAuthStore } from './oauth-store';
|
|
3
3
|
import { requireBearer, routeOAuth } from './oauth';
|
|
4
4
|
import { routeSetup } from './setup-page';
|
|
5
|
+
import { FAVICON_BYTES, FAVICON_CONTENT_TYPE } from './favicon';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Context passed to every fetch handler invocation. Built once at boot in
|
|
@@ -36,6 +37,16 @@ export async function handleRequest(
|
|
|
36
37
|
return jsonResponse({ ok: true });
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
if (url.pathname === '/favicon.ico' || url.pathname === '/favicon.png') {
|
|
41
|
+
return new Response(FAVICON_BYTES, {
|
|
42
|
+
status: 200,
|
|
43
|
+
headers: {
|
|
44
|
+
'content-type': FAVICON_CONTENT_TYPE,
|
|
45
|
+
'cache-control': 'public, max-age=86400',
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
39
50
|
// Root setup page — operator-facing UI (API-key gated) for picking
|
|
40
51
|
// profiles and generating the MCP URL.
|
|
41
52
|
const setupResponse = await routeSetup(req, ctx);
|
package/src/server/setup-page.ts
CHANGED
|
@@ -188,6 +188,7 @@ function loginFormHtml(errorMessage?: string): string {
|
|
|
188
188
|
<html lang="en">
|
|
189
189
|
<head>
|
|
190
190
|
<meta charset="utf-8">
|
|
191
|
+
<link rel="icon" type="image/png" href="/favicon.png">
|
|
191
192
|
<title>agentio MCP setup</title>
|
|
192
193
|
<style>${BASE_CSS}</style>
|
|
193
194
|
</head>
|
|
@@ -307,6 +308,7 @@ ${sectionsHtml}
|
|
|
307
308
|
<html lang="en">
|
|
308
309
|
<head>
|
|
309
310
|
<meta charset="utf-8">
|
|
311
|
+
<link rel="icon" type="image/png" href="/favicon.png">
|
|
310
312
|
<title>agentio MCP setup</title>
|
|
311
313
|
<style>${BASE_CSS}${PROFILES_CSS}</style>
|
|
312
314
|
</head>
|
package/src/types/config.ts
CHANGED
|
@@ -40,6 +40,23 @@ export interface ProfileEntry {
|
|
|
40
40
|
// Helper type for backward compatibility during migration
|
|
41
41
|
export type ProfileValue = string | ProfileEntry;
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Record of a successfully teleported siteio app. Persisted so that
|
|
45
|
+
* `agentio mcp teleport --sync` (and other name-optional variants) can
|
|
46
|
+
* default to the most recently deployed app instead of forcing the
|
|
47
|
+
* user to re-type the name every time.
|
|
48
|
+
*/
|
|
49
|
+
export interface TeleportAppRecord {
|
|
50
|
+
name: string;
|
|
51
|
+
url?: string;
|
|
52
|
+
/** Unix epoch milliseconds of the last successful deploy or sync. */
|
|
53
|
+
deployedAt: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface TeleportConfig {
|
|
57
|
+
lastApp?: TeleportAppRecord;
|
|
58
|
+
}
|
|
59
|
+
|
|
43
60
|
export interface Config {
|
|
44
61
|
profiles: {
|
|
45
62
|
gdocs?: ProfileValue[];
|
|
@@ -60,6 +77,7 @@ export interface Config {
|
|
|
60
77
|
env?: Record<string, string>;
|
|
61
78
|
gateway?: GatewayConfig;
|
|
62
79
|
server?: ServerConfig;
|
|
80
|
+
teleport?: TeleportConfig;
|
|
63
81
|
}
|
|
64
82
|
|
|
65
83
|
export type ServiceName = 'gdocs' | 'gdrive' | 'gmail' | 'gcal' | 'gtasks' | 'gchat' | 'gsheets' | 'github' | 'jira' | 'slack' | 'telegram' | 'whatsapp' | 'discourse' | 'sql';
|