@ottocode/server 0.1.213 → 0.1.216
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 +5 -5
- package/src/index.ts +4 -0
- package/src/openapi/paths/auth.ts +497 -0
- package/src/openapi/paths/branch.ts +102 -0
- package/src/openapi/paths/config.ts +43 -0
- package/src/openapi/paths/doctor.ts +165 -0
- package/src/openapi/paths/files.ts +97 -0
- package/src/openapi/paths/git.ts +52 -0
- package/src/openapi/paths/mcp.ts +339 -0
- package/src/openapi/paths/provider-usage.ts +59 -0
- package/src/openapi/paths/research.ts +227 -0
- package/src/openapi/paths/session-approval.ts +93 -0
- package/src/openapi/paths/session-extras.ts +336 -0
- package/src/openapi/paths/session-files.ts +91 -0
- package/src/openapi/paths/setu.ts +258 -0
- package/src/openapi/paths/skills.ts +256 -0
- package/src/openapi/paths/tunnel.ts +163 -0
- package/src/openapi/schemas.ts +36 -0
- package/src/openapi/spec.ts +31 -7
- package/src/routes/doctor.ts +229 -0
- package/src/routes/sessions.ts +55 -0
- package/src/routes/skills.ts +137 -0
- package/src/runtime/agent/runner-setup.ts +3 -2
- package/src/runtime/debug/state.ts +11 -0
- package/src/runtime/message/history-builder.ts +15 -16
- package/src/runtime/provider/setu.ts +2 -2
- package/src/runtime/tools/guards.ts +1 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { errorResponse } from '../helpers';
|
|
2
|
+
|
|
3
|
+
export const tunnelPaths = {
|
|
4
|
+
'/v1/tunnel/status': {
|
|
5
|
+
get: {
|
|
6
|
+
tags: ['tunnel'],
|
|
7
|
+
operationId: 'getTunnelStatus',
|
|
8
|
+
summary: 'Get tunnel status',
|
|
9
|
+
responses: {
|
|
10
|
+
200: {
|
|
11
|
+
description: 'OK',
|
|
12
|
+
content: {
|
|
13
|
+
'application/json': {
|
|
14
|
+
schema: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
status: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
enum: ['idle', 'starting', 'connected', 'error'],
|
|
20
|
+
},
|
|
21
|
+
url: { type: 'string', nullable: true },
|
|
22
|
+
error: { type: 'string', nullable: true },
|
|
23
|
+
binaryInstalled: { type: 'boolean' },
|
|
24
|
+
isRunning: { type: 'boolean' },
|
|
25
|
+
},
|
|
26
|
+
required: ['status', 'binaryInstalled', 'isRunning'],
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
'/v1/tunnel/start': {
|
|
35
|
+
post: {
|
|
36
|
+
tags: ['tunnel'],
|
|
37
|
+
operationId: 'startTunnel',
|
|
38
|
+
summary: 'Start a tunnel',
|
|
39
|
+
requestBody: {
|
|
40
|
+
required: false,
|
|
41
|
+
content: {
|
|
42
|
+
'application/json': {
|
|
43
|
+
schema: {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
port: { type: 'integer' },
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
responses: {
|
|
53
|
+
200: {
|
|
54
|
+
description: 'OK',
|
|
55
|
+
content: {
|
|
56
|
+
'application/json': {
|
|
57
|
+
schema: {
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {
|
|
60
|
+
ok: { type: 'boolean' },
|
|
61
|
+
url: { type: 'string' },
|
|
62
|
+
message: { type: 'string' },
|
|
63
|
+
error: { type: 'string' },
|
|
64
|
+
},
|
|
65
|
+
required: ['ok'],
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
'/v1/tunnel/register': {
|
|
74
|
+
post: {
|
|
75
|
+
tags: ['tunnel'],
|
|
76
|
+
operationId: 'registerTunnel',
|
|
77
|
+
summary: 'Register an external tunnel URL',
|
|
78
|
+
requestBody: {
|
|
79
|
+
required: true,
|
|
80
|
+
content: {
|
|
81
|
+
'application/json': {
|
|
82
|
+
schema: {
|
|
83
|
+
type: 'object',
|
|
84
|
+
properties: {
|
|
85
|
+
url: { type: 'string' },
|
|
86
|
+
},
|
|
87
|
+
required: ['url'],
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
responses: {
|
|
93
|
+
200: {
|
|
94
|
+
description: 'OK',
|
|
95
|
+
content: {
|
|
96
|
+
'application/json': {
|
|
97
|
+
schema: {
|
|
98
|
+
type: 'object',
|
|
99
|
+
properties: {
|
|
100
|
+
ok: { type: 'boolean' },
|
|
101
|
+
url: { type: 'string' },
|
|
102
|
+
message: { type: 'string' },
|
|
103
|
+
},
|
|
104
|
+
required: ['ok'],
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
400: errorResponse(),
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
'/v1/tunnel/stop': {
|
|
114
|
+
post: {
|
|
115
|
+
tags: ['tunnel'],
|
|
116
|
+
operationId: 'stopTunnel',
|
|
117
|
+
summary: 'Stop the tunnel',
|
|
118
|
+
responses: {
|
|
119
|
+
200: {
|
|
120
|
+
description: 'OK',
|
|
121
|
+
content: {
|
|
122
|
+
'application/json': {
|
|
123
|
+
schema: {
|
|
124
|
+
type: 'object',
|
|
125
|
+
properties: {
|
|
126
|
+
ok: { type: 'boolean' },
|
|
127
|
+
message: { type: 'string' },
|
|
128
|
+
},
|
|
129
|
+
required: ['ok'],
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
'/v1/tunnel/qr': {
|
|
138
|
+
get: {
|
|
139
|
+
tags: ['tunnel'],
|
|
140
|
+
operationId: 'getTunnelQR',
|
|
141
|
+
summary: 'Get QR code for tunnel URL',
|
|
142
|
+
responses: {
|
|
143
|
+
200: {
|
|
144
|
+
description: 'OK',
|
|
145
|
+
content: {
|
|
146
|
+
'application/json': {
|
|
147
|
+
schema: {
|
|
148
|
+
type: 'object',
|
|
149
|
+
properties: {
|
|
150
|
+
ok: { type: 'boolean' },
|
|
151
|
+
url: { type: 'string' },
|
|
152
|
+
qrCode: { type: 'string' },
|
|
153
|
+
},
|
|
154
|
+
required: ['ok'],
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
400: errorResponse(),
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
} as const;
|
package/src/openapi/schemas.ts
CHANGED
|
@@ -349,4 +349,40 @@ export const schemas = {
|
|
|
349
349
|
uptime: { type: 'integer' },
|
|
350
350
|
},
|
|
351
351
|
},
|
|
352
|
+
MCPServer: {
|
|
353
|
+
type: 'object',
|
|
354
|
+
properties: {
|
|
355
|
+
name: { type: 'string' },
|
|
356
|
+
transport: {
|
|
357
|
+
type: 'string',
|
|
358
|
+
enum: ['stdio', 'http', 'sse'],
|
|
359
|
+
},
|
|
360
|
+
command: { type: 'string' },
|
|
361
|
+
args: {
|
|
362
|
+
type: 'array',
|
|
363
|
+
items: { type: 'string' },
|
|
364
|
+
},
|
|
365
|
+
url: { type: 'string' },
|
|
366
|
+
disabled: { type: 'boolean' },
|
|
367
|
+
connected: { type: 'boolean' },
|
|
368
|
+
tools: {
|
|
369
|
+
type: 'array',
|
|
370
|
+
items: {
|
|
371
|
+
type: 'object',
|
|
372
|
+
properties: {
|
|
373
|
+
name: { type: 'string' },
|
|
374
|
+
description: { type: 'string' },
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
authRequired: { type: 'boolean' },
|
|
379
|
+
authenticated: { type: 'boolean' },
|
|
380
|
+
scope: {
|
|
381
|
+
type: 'string',
|
|
382
|
+
enum: ['global', 'project'],
|
|
383
|
+
},
|
|
384
|
+
authType: { type: 'string' },
|
|
385
|
+
},
|
|
386
|
+
required: ['name', 'transport', 'connected'],
|
|
387
|
+
},
|
|
352
388
|
} as const;
|
package/src/openapi/spec.ts
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
import { askPaths } from './paths/ask';
|
|
2
|
+
import { authPaths } from './paths/auth';
|
|
3
|
+
import { branchPaths } from './paths/branch';
|
|
2
4
|
import { configPaths } from './paths/config';
|
|
5
|
+
import { doctorPaths } from './paths/doctor';
|
|
3
6
|
import { filesPaths } from './paths/files';
|
|
4
7
|
import { gitPaths } from './paths/git';
|
|
8
|
+
import { mcpPaths } from './paths/mcp';
|
|
5
9
|
import { messagesPaths } from './paths/messages';
|
|
10
|
+
import { providerUsagePaths } from './paths/provider-usage';
|
|
11
|
+
import { researchPaths } from './paths/research';
|
|
12
|
+
import { sessionApprovalPaths } from './paths/session-approval';
|
|
13
|
+
import { sessionExtrasPaths } from './paths/session-extras';
|
|
14
|
+
import { sessionFilesPaths } from './paths/session-files';
|
|
6
15
|
import { sessionsPaths } from './paths/sessions';
|
|
16
|
+
import { setuPaths } from './paths/setu';
|
|
17
|
+
import { skillsPaths } from './paths/skills';
|
|
7
18
|
import { streamPaths } from './paths/stream';
|
|
8
|
-
import { schemas } from './schemas';
|
|
9
|
-
|
|
10
19
|
import { terminalsPath } from './paths/terminals';
|
|
11
|
-
import {
|
|
20
|
+
import { tunnelPaths } from './paths/tunnel';
|
|
21
|
+
import { schemas } from './schemas';
|
|
12
22
|
|
|
13
23
|
export function getOpenAPISpec() {
|
|
14
24
|
const spec = {
|
|
@@ -29,17 +39,31 @@ export function getOpenAPISpec() {
|
|
|
29
39
|
{ name: 'git' },
|
|
30
40
|
{ name: 'terminals' },
|
|
31
41
|
{ name: 'setu' },
|
|
42
|
+
{ name: 'auth' },
|
|
43
|
+
{ name: 'mcp' },
|
|
44
|
+
{ name: 'tunnel' },
|
|
32
45
|
],
|
|
33
46
|
paths: {
|
|
34
47
|
...askPaths,
|
|
35
|
-
...
|
|
36
|
-
...
|
|
37
|
-
...streamPaths,
|
|
48
|
+
...authPaths,
|
|
49
|
+
...branchPaths,
|
|
38
50
|
...configPaths,
|
|
51
|
+
...doctorPaths,
|
|
39
52
|
...filesPaths,
|
|
40
53
|
...gitPaths,
|
|
41
|
-
...
|
|
54
|
+
...mcpPaths,
|
|
55
|
+
...messagesPaths,
|
|
56
|
+
...providerUsagePaths,
|
|
57
|
+
...researchPaths,
|
|
58
|
+
...sessionApprovalPaths,
|
|
59
|
+
...sessionExtrasPaths,
|
|
60
|
+
...sessionFilesPaths,
|
|
61
|
+
...sessionsPaths,
|
|
42
62
|
...setuPaths,
|
|
63
|
+
...skillsPaths,
|
|
64
|
+
...streamPaths,
|
|
65
|
+
...terminalsPath,
|
|
66
|
+
...tunnelPaths,
|
|
43
67
|
},
|
|
44
68
|
components: {
|
|
45
69
|
schemas,
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import type { Hono } from 'hono';
|
|
2
|
+
import { readdir } from 'node:fs/promises';
|
|
3
|
+
import {
|
|
4
|
+
readConfig,
|
|
5
|
+
isAuthorized,
|
|
6
|
+
buildFsTools,
|
|
7
|
+
buildGitTools,
|
|
8
|
+
getSecureAuthPath,
|
|
9
|
+
getGlobalAgentsJsonPath,
|
|
10
|
+
getGlobalToolsDir,
|
|
11
|
+
getGlobalCommandsDir,
|
|
12
|
+
logger,
|
|
13
|
+
} from '@ottocode/sdk';
|
|
14
|
+
import type { ProviderId } from '@ottocode/sdk';
|
|
15
|
+
import { serializeError } from '../runtime/errors/api-error.ts';
|
|
16
|
+
|
|
17
|
+
const PROVIDERS: ProviderId[] = [
|
|
18
|
+
'openai',
|
|
19
|
+
'anthropic',
|
|
20
|
+
'google',
|
|
21
|
+
'openrouter',
|
|
22
|
+
'opencode',
|
|
23
|
+
'setu',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function providerEnvVar(p: ProviderId): string | null {
|
|
27
|
+
if (p === 'openai') return 'OPENAI_API_KEY';
|
|
28
|
+
if (p === 'anthropic') return 'ANTHROPIC_API_KEY';
|
|
29
|
+
if (p === 'google') return 'GOOGLE_GENERATIVE_AI_API_KEY';
|
|
30
|
+
if (p === 'opencode') return 'OPENCODE_API_KEY';
|
|
31
|
+
if (p === 'setu') return 'SETU_PRIVATE_KEY';
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function fileExists(path: string | null): Promise<boolean> {
|
|
36
|
+
if (!path) return false;
|
|
37
|
+
try {
|
|
38
|
+
return await Bun.file(path).exists();
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function readJsonSafe<T>(path: string | null): Promise<T | null> {
|
|
45
|
+
if (!path) return null;
|
|
46
|
+
try {
|
|
47
|
+
const file = Bun.file(path);
|
|
48
|
+
if (!(await file.exists())) return null;
|
|
49
|
+
return (await file.json()) as T;
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function listDir(dir: string | null): Promise<string[]> {
|
|
56
|
+
if (!dir) return [];
|
|
57
|
+
try {
|
|
58
|
+
return await readdir(dir);
|
|
59
|
+
} catch {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function registerDoctorRoutes(app: Hono) {
|
|
65
|
+
app.get('/v1/doctor', async (c) => {
|
|
66
|
+
try {
|
|
67
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
68
|
+
const { cfg, auth } = await readConfig(projectRoot);
|
|
69
|
+
|
|
70
|
+
const providers = await Promise.all(
|
|
71
|
+
PROVIDERS.map(async (id) => {
|
|
72
|
+
const ok = await isAuthorized(id, projectRoot);
|
|
73
|
+
const envVar = providerEnvVar(id);
|
|
74
|
+
const envConfigured = envVar ? !!process.env[envVar] : false;
|
|
75
|
+
|
|
76
|
+
const globalAuthPath = getSecureAuthPath();
|
|
77
|
+
let hasGlobalAuth = false;
|
|
78
|
+
if (globalAuthPath) {
|
|
79
|
+
const contents =
|
|
80
|
+
await readJsonSafe<Record<string, unknown>>(globalAuthPath);
|
|
81
|
+
hasGlobalAuth = Boolean(contents?.[id]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const authInfo = auth?.[id];
|
|
85
|
+
const hasStoredSecret = (() => {
|
|
86
|
+
if (!authInfo) return false;
|
|
87
|
+
if (authInfo.type === 'api')
|
|
88
|
+
return Boolean((authInfo as { key?: string }).key);
|
|
89
|
+
if (authInfo.type === 'wallet')
|
|
90
|
+
return Boolean((authInfo as { secret?: string }).secret);
|
|
91
|
+
if (authInfo.type === 'oauth')
|
|
92
|
+
return Boolean(
|
|
93
|
+
(authInfo as { access?: string; refresh?: string }).access ||
|
|
94
|
+
(authInfo as { access?: string; refresh?: string }).refresh,
|
|
95
|
+
);
|
|
96
|
+
return false;
|
|
97
|
+
})();
|
|
98
|
+
|
|
99
|
+
const sources: string[] = [];
|
|
100
|
+
if (envConfigured && envVar) sources.push(`env:${envVar}`);
|
|
101
|
+
if (hasGlobalAuth) sources.push('auth.json');
|
|
102
|
+
|
|
103
|
+
const configured =
|
|
104
|
+
envConfigured ||
|
|
105
|
+
hasGlobalAuth ||
|
|
106
|
+
cfg.defaults.provider === id ||
|
|
107
|
+
hasStoredSecret;
|
|
108
|
+
|
|
109
|
+
return { id, ok, configured, sources };
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const defaults = {
|
|
114
|
+
agent: cfg.defaults.agent,
|
|
115
|
+
provider: cfg.defaults.provider,
|
|
116
|
+
model: cfg.defaults.model,
|
|
117
|
+
providerAuthorized: await isAuthorized(
|
|
118
|
+
cfg.defaults.provider as ProviderId,
|
|
119
|
+
projectRoot,
|
|
120
|
+
),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const globalAgentsPath = getGlobalAgentsJsonPath();
|
|
124
|
+
const localAgentsPath = `${projectRoot}/.otto/agents.json`;
|
|
125
|
+
const globalAgents =
|
|
126
|
+
(await readJsonSafe<Record<string, unknown>>(globalAgentsPath)) ?? {};
|
|
127
|
+
const localAgents =
|
|
128
|
+
(await readJsonSafe<Record<string, unknown>>(localAgentsPath)) ?? {};
|
|
129
|
+
|
|
130
|
+
const agents = {
|
|
131
|
+
globalPath: (await fileExists(globalAgentsPath))
|
|
132
|
+
? globalAgentsPath
|
|
133
|
+
: null,
|
|
134
|
+
localPath: (await fileExists(localAgentsPath)) ? localAgentsPath : null,
|
|
135
|
+
globalNames: Object.keys(globalAgents).sort(),
|
|
136
|
+
localNames: Object.keys(localAgents).sort(),
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const defaultToolNames = Array.from(
|
|
140
|
+
new Set([
|
|
141
|
+
...buildFsTools(projectRoot).map((t) => t.name),
|
|
142
|
+
...buildGitTools(projectRoot).map((t) => t.name),
|
|
143
|
+
'finish',
|
|
144
|
+
]),
|
|
145
|
+
).sort();
|
|
146
|
+
|
|
147
|
+
const globalToolsDir = getGlobalToolsDir();
|
|
148
|
+
const localToolsDir = `${projectRoot}/.otto/tools`;
|
|
149
|
+
const globalToolNames = await listDir(globalToolsDir);
|
|
150
|
+
const localToolNames = await listDir(localToolsDir);
|
|
151
|
+
|
|
152
|
+
const tools = {
|
|
153
|
+
defaultNames: defaultToolNames,
|
|
154
|
+
globalPath: globalToolNames.length ? globalToolsDir : null,
|
|
155
|
+
globalNames: globalToolNames.sort(),
|
|
156
|
+
localPath: localToolNames.length ? localToolsDir : null,
|
|
157
|
+
localNames: localToolNames.sort(),
|
|
158
|
+
effectiveNames: Array.from(
|
|
159
|
+
new Set([...defaultToolNames, ...globalToolNames, ...localToolNames]),
|
|
160
|
+
).sort(),
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const globalCommandsDir = getGlobalCommandsDir();
|
|
164
|
+
const localCommandsDir = `${projectRoot}/.otto/commands`;
|
|
165
|
+
const globalCommandFiles = await listDir(globalCommandsDir);
|
|
166
|
+
const localCommandFiles = await listDir(localCommandsDir);
|
|
167
|
+
|
|
168
|
+
const commands = {
|
|
169
|
+
globalPath: globalCommandFiles.length ? globalCommandsDir : null,
|
|
170
|
+
globalNames: globalCommandFiles
|
|
171
|
+
.filter((f) => f.endsWith('.json'))
|
|
172
|
+
.map((f) => f.replace(/\.json$/, ''))
|
|
173
|
+
.sort(),
|
|
174
|
+
localPath: localCommandFiles.length ? localCommandsDir : null,
|
|
175
|
+
localNames: localCommandFiles
|
|
176
|
+
.filter((f) => f.endsWith('.json'))
|
|
177
|
+
.map((f) => f.replace(/\.json$/, ''))
|
|
178
|
+
.sort(),
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const issues: string[] = [];
|
|
182
|
+
if (!defaults.providerAuthorized) {
|
|
183
|
+
issues.push(
|
|
184
|
+
`Default provider '${defaults.provider}' is not authorized`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
for (const [scope, entries] of [
|
|
188
|
+
['global', globalAgents],
|
|
189
|
+
['local', localAgents],
|
|
190
|
+
] as const) {
|
|
191
|
+
for (const [name, entry] of Object.entries(entries)) {
|
|
192
|
+
if (
|
|
193
|
+
entry &&
|
|
194
|
+
typeof entry === 'object' &&
|
|
195
|
+
Object.hasOwn(entry, 'tools') &&
|
|
196
|
+
!Array.isArray((entry as { tools?: unknown }).tools)
|
|
197
|
+
) {
|
|
198
|
+
issues.push(`${scope}:${name} tools field must be an array`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const suggestions: string[] = [];
|
|
204
|
+
if (!defaults.providerAuthorized) {
|
|
205
|
+
suggestions.push(
|
|
206
|
+
`Run: otto auth login ${defaults.provider} — or switch defaults with: otto models`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
if (issues.length) {
|
|
210
|
+
suggestions.push('Review agents.json fields.');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return c.json({
|
|
214
|
+
providers,
|
|
215
|
+
defaults,
|
|
216
|
+
agents,
|
|
217
|
+
tools,
|
|
218
|
+
commands,
|
|
219
|
+
issues,
|
|
220
|
+
suggestions,
|
|
221
|
+
globalAuthPath: getSecureAuthPath(),
|
|
222
|
+
});
|
|
223
|
+
} catch (error) {
|
|
224
|
+
logger.error('Failed to run doctor', error);
|
|
225
|
+
const errorResponse = serializeError(error);
|
|
226
|
+
return c.json(errorResponse, (errorResponse.error.status || 500) as 500);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}
|
package/src/routes/sessions.ts
CHANGED
|
@@ -721,6 +721,61 @@ export function registerSessionsRoutes(app: Hono) {
|
|
|
721
721
|
});
|
|
722
722
|
});
|
|
723
723
|
|
|
724
|
+
app.delete('/v1/sessions/:sessionId/share', async (c) => {
|
|
725
|
+
const sessionId = c.req.param('sessionId');
|
|
726
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
727
|
+
const cfg = await loadConfig(projectRoot);
|
|
728
|
+
const db = await getDb(cfg.projectRoot);
|
|
729
|
+
|
|
730
|
+
const share = await db
|
|
731
|
+
.select()
|
|
732
|
+
.from(shares)
|
|
733
|
+
.where(eq(shares.sessionId, sessionId))
|
|
734
|
+
.limit(1);
|
|
735
|
+
|
|
736
|
+
if (!share.length) {
|
|
737
|
+
return c.json({ error: 'Session is not shared' }, 404);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
try {
|
|
741
|
+
const res = await fetch(`${SHARE_API_URL}/share/${share[0].shareId}`, {
|
|
742
|
+
method: 'DELETE',
|
|
743
|
+
headers: { 'X-Share-Secret': share[0].secret },
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
if (!res.ok && res.status !== 404) {
|
|
747
|
+
const err = await res.text();
|
|
748
|
+
return c.json({ error: `Failed to delete share: ${err}` }, 500);
|
|
749
|
+
}
|
|
750
|
+
} catch {}
|
|
751
|
+
|
|
752
|
+
await db.delete(shares).where(eq(shares.sessionId, sessionId));
|
|
753
|
+
|
|
754
|
+
return c.json({ deleted: true, sessionId });
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
app.get('/v1/shares', async (c) => {
|
|
758
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
759
|
+
const cfg = await loadConfig(projectRoot);
|
|
760
|
+
const db = await getDb(cfg.projectRoot);
|
|
761
|
+
|
|
762
|
+
const rows = await db
|
|
763
|
+
.select({
|
|
764
|
+
sessionId: shares.sessionId,
|
|
765
|
+
shareId: shares.shareId,
|
|
766
|
+
url: shares.url,
|
|
767
|
+
title: shares.title,
|
|
768
|
+
createdAt: shares.createdAt,
|
|
769
|
+
lastSyncedAt: shares.lastSyncedAt,
|
|
770
|
+
})
|
|
771
|
+
.from(shares)
|
|
772
|
+
.innerJoin(sessions, eq(shares.sessionId, sessions.id))
|
|
773
|
+
.where(eq(sessions.projectPath, cfg.projectRoot))
|
|
774
|
+
.orderBy(desc(shares.lastSyncedAt));
|
|
775
|
+
|
|
776
|
+
return c.json({ shares: rows });
|
|
777
|
+
});
|
|
778
|
+
|
|
724
779
|
// Retry a failed assistant message
|
|
725
780
|
app.post('/v1/sessions/:sessionId/messages/:messageId/retry', async (c) => {
|
|
726
781
|
try {
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { Hono } from 'hono';
|
|
2
|
+
import {
|
|
3
|
+
discoverSkills,
|
|
4
|
+
loadSkill,
|
|
5
|
+
loadSkillFile,
|
|
6
|
+
discoverSkillFiles,
|
|
7
|
+
findGitRoot,
|
|
8
|
+
validateSkillName,
|
|
9
|
+
parseSkillFile,
|
|
10
|
+
logger,
|
|
11
|
+
} from '@ottocode/sdk';
|
|
12
|
+
import { serializeError } from '../runtime/errors/api-error.ts';
|
|
13
|
+
|
|
14
|
+
export function registerSkillsRoutes(app: Hono) {
|
|
15
|
+
app.get('/v1/skills', async (c) => {
|
|
16
|
+
try {
|
|
17
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
18
|
+
const repoRoot = (await findGitRoot(projectRoot)) ?? projectRoot;
|
|
19
|
+
const skills = await discoverSkills(projectRoot, repoRoot);
|
|
20
|
+
return c.json({
|
|
21
|
+
skills: skills.map((s) => ({
|
|
22
|
+
name: s.name,
|
|
23
|
+
description: s.description,
|
|
24
|
+
scope: s.scope,
|
|
25
|
+
path: s.path,
|
|
26
|
+
})),
|
|
27
|
+
});
|
|
28
|
+
} catch (error) {
|
|
29
|
+
logger.error('Failed to list skills', error);
|
|
30
|
+
const errorResponse = serializeError(error);
|
|
31
|
+
return c.json(errorResponse, (errorResponse.error.status || 500) as 500);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
app.get('/v1/skills/:name', async (c) => {
|
|
36
|
+
try {
|
|
37
|
+
const name = c.req.param('name');
|
|
38
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
39
|
+
const repoRoot = (await findGitRoot(projectRoot)) ?? projectRoot;
|
|
40
|
+
await discoverSkills(projectRoot, repoRoot);
|
|
41
|
+
|
|
42
|
+
const skill = await loadSkill(name);
|
|
43
|
+
if (!skill) {
|
|
44
|
+
return c.json({ error: `Skill '${name}' not found` }, 404);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return c.json({
|
|
48
|
+
name: skill.metadata.name,
|
|
49
|
+
description: skill.metadata.description,
|
|
50
|
+
license: skill.metadata.license ?? null,
|
|
51
|
+
compatibility: skill.metadata.compatibility ?? null,
|
|
52
|
+
metadata: skill.metadata.metadata ?? null,
|
|
53
|
+
allowedTools: skill.metadata.allowedTools ?? null,
|
|
54
|
+
path: skill.path,
|
|
55
|
+
scope: skill.scope,
|
|
56
|
+
content: skill.content,
|
|
57
|
+
});
|
|
58
|
+
} catch (error) {
|
|
59
|
+
logger.error('Failed to load skill', error);
|
|
60
|
+
const errorResponse = serializeError(error);
|
|
61
|
+
return c.json(errorResponse, (errorResponse.error.status || 500) as 500);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
app.post('/v1/skills/validate', async (c) => {
|
|
66
|
+
app.get('/v1/skills/:name/files', async (c) => {
|
|
67
|
+
try {
|
|
68
|
+
const name = c.req.param('name');
|
|
69
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
70
|
+
const repoRoot = (await findGitRoot(projectRoot)) ?? projectRoot;
|
|
71
|
+
await discoverSkills(projectRoot, repoRoot);
|
|
72
|
+
|
|
73
|
+
const files = await discoverSkillFiles(name);
|
|
74
|
+
return c.json({ files });
|
|
75
|
+
} catch (error) {
|
|
76
|
+
logger.error('Failed to list skill files', error);
|
|
77
|
+
const errorResponse = serializeError(error);
|
|
78
|
+
return c.json(
|
|
79
|
+
errorResponse,
|
|
80
|
+
(errorResponse.error.status || 500) as 500,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
app.get('/v1/skills/:name/files/*', async (c) => {
|
|
86
|
+
try {
|
|
87
|
+
const name = c.req.param('name');
|
|
88
|
+
const filePath = c.req.path.replace(`/v1/skills/${name}/files/`, '');
|
|
89
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
90
|
+
const repoRoot = (await findGitRoot(projectRoot)) ?? projectRoot;
|
|
91
|
+
await discoverSkills(projectRoot, repoRoot);
|
|
92
|
+
|
|
93
|
+
const result = await loadSkillFile(name, filePath);
|
|
94
|
+
if (!result) {
|
|
95
|
+
return c.json(
|
|
96
|
+
{ error: `File '${filePath}' not found in skill '${name}'` },
|
|
97
|
+
404,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return c.json({ content: result.content, path: result.resolvedPath });
|
|
101
|
+
} catch (error) {
|
|
102
|
+
logger.error('Failed to load skill file', error);
|
|
103
|
+
const errorResponse = serializeError(error);
|
|
104
|
+
return c.json(
|
|
105
|
+
errorResponse,
|
|
106
|
+
(errorResponse.error.status || 500) as 500,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const body = await c.req.json<{ content: string; path?: string }>();
|
|
113
|
+
if (!body.content) {
|
|
114
|
+
return c.json({ error: 'content is required' }, 400);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const skillPath = body.path ?? 'SKILL.md';
|
|
118
|
+
const skill = parseSkillFile(body.content, skillPath, 'cwd');
|
|
119
|
+
return c.json({
|
|
120
|
+
valid: true,
|
|
121
|
+
name: skill.metadata.name,
|
|
122
|
+
description: skill.metadata.description,
|
|
123
|
+
license: skill.metadata.license ?? null,
|
|
124
|
+
});
|
|
125
|
+
} catch (error) {
|
|
126
|
+
return c.json({
|
|
127
|
+
valid: false,
|
|
128
|
+
error: (error as Error).message,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
app.get('/v1/skills/validate-name/:name', async (c) => {
|
|
134
|
+
const name = c.req.param('name');
|
|
135
|
+
return c.json({ valid: validateSkillName(name) });
|
|
136
|
+
});
|
|
137
|
+
}
|