@ottocode/server 0.1.173
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 +42 -0
- package/src/events/bus.ts +43 -0
- package/src/events/types.ts +32 -0
- package/src/index.ts +281 -0
- package/src/openapi/helpers.ts +64 -0
- package/src/openapi/paths/ask.ts +70 -0
- package/src/openapi/paths/config.ts +218 -0
- package/src/openapi/paths/files.ts +72 -0
- package/src/openapi/paths/git.ts +457 -0
- package/src/openapi/paths/messages.ts +92 -0
- package/src/openapi/paths/sessions.ts +90 -0
- package/src/openapi/paths/setu.ts +154 -0
- package/src/openapi/paths/stream.ts +26 -0
- package/src/openapi/paths/terminals.ts +226 -0
- package/src/openapi/schemas.ts +345 -0
- package/src/openapi/spec.ts +49 -0
- package/src/presets.ts +85 -0
- package/src/routes/ask.ts +113 -0
- package/src/routes/auth.ts +592 -0
- package/src/routes/branch.ts +106 -0
- package/src/routes/config/agents.ts +44 -0
- package/src/routes/config/cwd.ts +21 -0
- package/src/routes/config/defaults.ts +45 -0
- package/src/routes/config/index.ts +16 -0
- package/src/routes/config/main.ts +73 -0
- package/src/routes/config/models.ts +139 -0
- package/src/routes/config/providers.ts +46 -0
- package/src/routes/config/utils.ts +120 -0
- package/src/routes/files.ts +218 -0
- package/src/routes/git/branch.ts +75 -0
- package/src/routes/git/commit.ts +209 -0
- package/src/routes/git/diff.ts +137 -0
- package/src/routes/git/index.ts +18 -0
- package/src/routes/git/push.ts +160 -0
- package/src/routes/git/schemas.ts +48 -0
- package/src/routes/git/staging.ts +208 -0
- package/src/routes/git/status.ts +83 -0
- package/src/routes/git/types.ts +31 -0
- package/src/routes/git/utils.ts +249 -0
- package/src/routes/openapi.ts +6 -0
- package/src/routes/research.ts +392 -0
- package/src/routes/root.ts +5 -0
- package/src/routes/session-approval.ts +63 -0
- package/src/routes/session-files.ts +387 -0
- package/src/routes/session-messages.ts +170 -0
- package/src/routes/session-stream.ts +61 -0
- package/src/routes/sessions.ts +814 -0
- package/src/routes/setu.ts +346 -0
- package/src/routes/terminals.ts +227 -0
- package/src/runtime/agent/registry.ts +351 -0
- package/src/runtime/agent/runner-reasoning.ts +108 -0
- package/src/runtime/agent/runner-setup.ts +257 -0
- package/src/runtime/agent/runner.ts +375 -0
- package/src/runtime/agent-registry.ts +6 -0
- package/src/runtime/ask/service.ts +369 -0
- package/src/runtime/context/environment.ts +202 -0
- package/src/runtime/debug/index.ts +117 -0
- package/src/runtime/debug/state.ts +140 -0
- package/src/runtime/errors/api-error.ts +192 -0
- package/src/runtime/errors/handling.ts +199 -0
- package/src/runtime/message/compaction-auto.ts +154 -0
- package/src/runtime/message/compaction-context.ts +101 -0
- package/src/runtime/message/compaction-detect.ts +26 -0
- package/src/runtime/message/compaction-limits.ts +37 -0
- package/src/runtime/message/compaction-mark.ts +111 -0
- package/src/runtime/message/compaction-prune.ts +75 -0
- package/src/runtime/message/compaction.ts +21 -0
- package/src/runtime/message/history-builder.ts +266 -0
- package/src/runtime/message/service.ts +468 -0
- package/src/runtime/message/tool-history-tracker.ts +204 -0
- package/src/runtime/prompt/builder.ts +167 -0
- package/src/runtime/provider/anthropic.ts +50 -0
- package/src/runtime/provider/copilot.ts +12 -0
- package/src/runtime/provider/google.ts +8 -0
- package/src/runtime/provider/index.ts +60 -0
- package/src/runtime/provider/moonshot.ts +8 -0
- package/src/runtime/provider/oauth-adapter.ts +237 -0
- package/src/runtime/provider/openai.ts +18 -0
- package/src/runtime/provider/opencode.ts +7 -0
- package/src/runtime/provider/openrouter.ts +7 -0
- package/src/runtime/provider/selection.ts +118 -0
- package/src/runtime/provider/setu.ts +126 -0
- package/src/runtime/provider/zai.ts +16 -0
- package/src/runtime/session/branch.ts +280 -0
- package/src/runtime/session/db-operations.ts +285 -0
- package/src/runtime/session/manager.ts +99 -0
- package/src/runtime/session/queue.ts +243 -0
- package/src/runtime/stream/abort-handler.ts +65 -0
- package/src/runtime/stream/error-handler.ts +371 -0
- package/src/runtime/stream/finish-handler.ts +101 -0
- package/src/runtime/stream/handlers.ts +5 -0
- package/src/runtime/stream/step-finish.ts +93 -0
- package/src/runtime/stream/types.ts +25 -0
- package/src/runtime/tools/approval.ts +180 -0
- package/src/runtime/tools/context.ts +83 -0
- package/src/runtime/tools/mapping.ts +154 -0
- package/src/runtime/tools/setup.ts +44 -0
- package/src/runtime/topup/manager.ts +110 -0
- package/src/runtime/utils/cwd.ts +69 -0
- package/src/runtime/utils/token.ts +35 -0
- package/src/tools/adapter.ts +634 -0
- package/src/tools/database/get-parent-session.ts +183 -0
- package/src/tools/database/get-session-context.ts +161 -0
- package/src/tools/database/index.ts +42 -0
- package/src/tools/database/present-session-links.ts +47 -0
- package/src/tools/database/query-messages.ts +160 -0
- package/src/tools/database/query-sessions.ts +126 -0
- package/src/tools/database/search-history.ts +135 -0
- package/src/types/sql-imports.d.ts +5 -0
- package/sst-env.d.ts +8 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { publish } from '../../events/bus.ts';
|
|
2
|
+
import { debugLog } from '../debug/index.ts';
|
|
3
|
+
|
|
4
|
+
export type ToolApprovalMode = 'auto' | 'dangerous' | 'all';
|
|
5
|
+
|
|
6
|
+
export const DANGEROUS_TOOLS = new Set([
|
|
7
|
+
'bash',
|
|
8
|
+
'write',
|
|
9
|
+
'apply_patch',
|
|
10
|
+
'terminal',
|
|
11
|
+
'edit',
|
|
12
|
+
'git_commit',
|
|
13
|
+
'git_push',
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
export const SAFE_TOOLS = new Set([
|
|
17
|
+
'finish',
|
|
18
|
+
'progress_update',
|
|
19
|
+
'update_todos',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
export interface PendingApproval {
|
|
23
|
+
callId: string;
|
|
24
|
+
toolName: string;
|
|
25
|
+
args: unknown;
|
|
26
|
+
sessionId: string;
|
|
27
|
+
messageId: string;
|
|
28
|
+
resolve: (approved: boolean) => void;
|
|
29
|
+
createdAt: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const pendingApprovals = new Map<string, PendingApproval>();
|
|
33
|
+
|
|
34
|
+
export function requiresApproval(
|
|
35
|
+
toolName: string,
|
|
36
|
+
mode: ToolApprovalMode,
|
|
37
|
+
): boolean {
|
|
38
|
+
if (SAFE_TOOLS.has(toolName)) return false;
|
|
39
|
+
if (mode === 'auto') return false;
|
|
40
|
+
if (mode === 'all') return true;
|
|
41
|
+
if (mode === 'dangerous') return DANGEROUS_TOOLS.has(toolName);
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function requestApproval(
|
|
46
|
+
sessionId: string,
|
|
47
|
+
messageId: string,
|
|
48
|
+
callId: string,
|
|
49
|
+
toolName: string,
|
|
50
|
+
args: unknown,
|
|
51
|
+
timeoutMs = 120000,
|
|
52
|
+
): Promise<boolean> {
|
|
53
|
+
debugLog('[approval] requestApproval called', {
|
|
54
|
+
sessionId,
|
|
55
|
+
messageId,
|
|
56
|
+
callId,
|
|
57
|
+
toolName,
|
|
58
|
+
});
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
const approval: PendingApproval = {
|
|
61
|
+
callId,
|
|
62
|
+
toolName,
|
|
63
|
+
args,
|
|
64
|
+
sessionId,
|
|
65
|
+
messageId,
|
|
66
|
+
resolve,
|
|
67
|
+
createdAt: Date.now(),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
pendingApprovals.set(callId, approval);
|
|
71
|
+
debugLog(
|
|
72
|
+
'[approval] Added to pendingApprovals, count:',
|
|
73
|
+
pendingApprovals.size,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
publish({
|
|
77
|
+
type: 'tool.approval.required',
|
|
78
|
+
sessionId,
|
|
79
|
+
payload: {
|
|
80
|
+
callId,
|
|
81
|
+
toolName,
|
|
82
|
+
args,
|
|
83
|
+
messageId,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
setTimeout(() => {
|
|
88
|
+
if (pendingApprovals.has(callId)) {
|
|
89
|
+
pendingApprovals.delete(callId);
|
|
90
|
+
resolve(false);
|
|
91
|
+
publish({
|
|
92
|
+
type: 'tool.approval.resolved',
|
|
93
|
+
sessionId,
|
|
94
|
+
payload: {
|
|
95
|
+
callId,
|
|
96
|
+
toolName,
|
|
97
|
+
approved: false,
|
|
98
|
+
reason: 'timeout',
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}, timeoutMs);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function resolveApproval(
|
|
107
|
+
callId: string,
|
|
108
|
+
approved: boolean,
|
|
109
|
+
): { ok: boolean; error?: string } {
|
|
110
|
+
debugLog('[approval] resolveApproval called', {
|
|
111
|
+
callId,
|
|
112
|
+
approved,
|
|
113
|
+
pendingCount: pendingApprovals.size,
|
|
114
|
+
pendingIds: [...pendingApprovals.keys()],
|
|
115
|
+
});
|
|
116
|
+
const approval = pendingApprovals.get(callId);
|
|
117
|
+
if (!approval) {
|
|
118
|
+
debugLog('[approval] No pending approval found for callId:', callId);
|
|
119
|
+
return { ok: false, error: 'No pending approval found for this callId' };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
pendingApprovals.delete(callId);
|
|
123
|
+
approval.resolve(approved);
|
|
124
|
+
|
|
125
|
+
publish({
|
|
126
|
+
type: 'tool.approval.resolved',
|
|
127
|
+
sessionId: approval.sessionId,
|
|
128
|
+
payload: {
|
|
129
|
+
callId,
|
|
130
|
+
toolName: approval.toolName,
|
|
131
|
+
approved,
|
|
132
|
+
reason: approved ? 'user_approved' : 'user_rejected',
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return { ok: true };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function getPendingApproval(
|
|
140
|
+
callId: string,
|
|
141
|
+
): PendingApproval | undefined {
|
|
142
|
+
return pendingApprovals.get(callId);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function updateApprovalArgs(callId: string, args: unknown): boolean {
|
|
146
|
+
const approval = pendingApprovals.get(callId);
|
|
147
|
+
if (!approval) return false;
|
|
148
|
+
|
|
149
|
+
approval.args = args;
|
|
150
|
+
|
|
151
|
+
publish({
|
|
152
|
+
type: 'tool.approval.updated',
|
|
153
|
+
sessionId: approval.sessionId,
|
|
154
|
+
payload: {
|
|
155
|
+
callId,
|
|
156
|
+
toolName: approval.toolName,
|
|
157
|
+
args,
|
|
158
|
+
messageId: approval.messageId,
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function getPendingApprovalsForSession(
|
|
166
|
+
sessionId: string,
|
|
167
|
+
): PendingApproval[] {
|
|
168
|
+
return Array.from(pendingApprovals.values()).filter(
|
|
169
|
+
(a) => a.sessionId === sessionId,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function clearPendingApprovalsForSession(sessionId: string): void {
|
|
174
|
+
for (const [callId, approval] of pendingApprovals) {
|
|
175
|
+
if (approval.sessionId === sessionId) {
|
|
176
|
+
approval.resolve(false);
|
|
177
|
+
pendingApprovals.delete(callId);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm';
|
|
2
|
+
import type { DB } from '@ottocode/database';
|
|
3
|
+
import { messageParts } from '@ottocode/database/schema';
|
|
4
|
+
import type { ToolApprovalMode } from './approval.ts';
|
|
5
|
+
import { publish } from '../../events/bus.ts';
|
|
6
|
+
|
|
7
|
+
export type StepExecutionState = {
|
|
8
|
+
chain: Promise<void>;
|
|
9
|
+
failed: boolean;
|
|
10
|
+
failedToolName?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type ToolAdapterContext = {
|
|
14
|
+
sessionId: string;
|
|
15
|
+
messageId: string;
|
|
16
|
+
assistantPartId: string;
|
|
17
|
+
db: DB;
|
|
18
|
+
agent: string;
|
|
19
|
+
provider: string;
|
|
20
|
+
model: string;
|
|
21
|
+
projectRoot: string;
|
|
22
|
+
nextIndex: () => number | Promise<number>;
|
|
23
|
+
stepIndex?: number;
|
|
24
|
+
onFirstToolCall?: () => void;
|
|
25
|
+
stepExecution?: {
|
|
26
|
+
states: Map<number, StepExecutionState>;
|
|
27
|
+
};
|
|
28
|
+
toolApprovalMode?: ToolApprovalMode;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function extractFinishText(input: unknown): string | undefined {
|
|
32
|
+
if (typeof input === 'string') return input;
|
|
33
|
+
if (!input || typeof input !== 'object') return undefined;
|
|
34
|
+
const obj = input as Record<string, unknown>;
|
|
35
|
+
if (typeof obj.text === 'string') return obj.text;
|
|
36
|
+
if (
|
|
37
|
+
obj.input &&
|
|
38
|
+
typeof (obj.input as Record<string, unknown>).text === 'string'
|
|
39
|
+
)
|
|
40
|
+
return String((obj.input as Record<string, unknown>).text);
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function appendAssistantText(
|
|
45
|
+
ctx: ToolAdapterContext,
|
|
46
|
+
text: string,
|
|
47
|
+
): Promise<void> {
|
|
48
|
+
try {
|
|
49
|
+
const rows = await ctx.db
|
|
50
|
+
.select()
|
|
51
|
+
.from(messageParts)
|
|
52
|
+
.where(eq(messageParts.id, ctx.assistantPartId));
|
|
53
|
+
let previous = '';
|
|
54
|
+
if (rows.length) {
|
|
55
|
+
try {
|
|
56
|
+
const parsed = JSON.parse(rows[0]?.content ?? '{}');
|
|
57
|
+
if (parsed && typeof parsed.text === 'string') previous = parsed.text;
|
|
58
|
+
} catch {}
|
|
59
|
+
}
|
|
60
|
+
const addition = text.startsWith(previous)
|
|
61
|
+
? text.slice(previous.length)
|
|
62
|
+
: text;
|
|
63
|
+
if (addition.length) {
|
|
64
|
+
const payload: Record<string, unknown> = {
|
|
65
|
+
messageId: ctx.messageId,
|
|
66
|
+
partId: ctx.assistantPartId,
|
|
67
|
+
delta: addition,
|
|
68
|
+
};
|
|
69
|
+
if (ctx.stepIndex !== undefined) payload.stepIndex = ctx.stepIndex;
|
|
70
|
+
publish({
|
|
71
|
+
type: 'message.part.delta',
|
|
72
|
+
sessionId: ctx.sessionId,
|
|
73
|
+
payload,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
await ctx.db
|
|
77
|
+
.update(messageParts)
|
|
78
|
+
.set({ content: JSON.stringify({ text }) })
|
|
79
|
+
.where(eq(messageParts.id, ctx.assistantPartId));
|
|
80
|
+
} catch {
|
|
81
|
+
// ignore to keep run alive if we can't persist the text
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool name mapping for Claude Code OAuth compatibility.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code OAuth requires PascalCase tool names but does NOT whitelist
|
|
5
|
+
* specific tools. Any tool with a PascalCase name is accepted.
|
|
6
|
+
*
|
|
7
|
+
* This module provides bidirectional mapping between otto's canonical
|
|
8
|
+
* snake_case names and the PascalCase format required for OAuth.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type ToolNamingConvention = 'canonical' | 'claude-code';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Mapping from otto canonical names to PascalCase names.
|
|
15
|
+
* Includes ALL otto tools for complete OAuth compatibility.
|
|
16
|
+
*/
|
|
17
|
+
export const CANONICAL_TO_PASCAL: Record<string, string> = {
|
|
18
|
+
// File system operations
|
|
19
|
+
read: 'Read',
|
|
20
|
+
write: 'Write',
|
|
21
|
+
ls: 'Ls',
|
|
22
|
+
tree: 'Tree',
|
|
23
|
+
cd: 'Cd',
|
|
24
|
+
pwd: 'Pwd',
|
|
25
|
+
|
|
26
|
+
// Search operations
|
|
27
|
+
glob: 'Glob',
|
|
28
|
+
ripgrep: 'Grep', // Maps to Grep for Claude Code compatibility
|
|
29
|
+
grep: 'Grep',
|
|
30
|
+
|
|
31
|
+
// Execution
|
|
32
|
+
bash: 'Bash',
|
|
33
|
+
terminal: 'Terminal',
|
|
34
|
+
|
|
35
|
+
// Git operations
|
|
36
|
+
git_status: 'GitStatus',
|
|
37
|
+
git_diff: 'GitDiff',
|
|
38
|
+
git_commit: 'GitCommit',
|
|
39
|
+
|
|
40
|
+
// Patch/edit
|
|
41
|
+
apply_patch: 'ApplyPatch',
|
|
42
|
+
|
|
43
|
+
// Task management
|
|
44
|
+
update_todos: 'UpdateTodos',
|
|
45
|
+
progress_update: 'ProgressUpdate',
|
|
46
|
+
finish: 'Finish',
|
|
47
|
+
|
|
48
|
+
// Web operations
|
|
49
|
+
websearch: 'WebSearch',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Reverse mapping from PascalCase names to canonical.
|
|
54
|
+
* Built to handle the many-to-one ripgrep/grep → Grep mapping.
|
|
55
|
+
*/
|
|
56
|
+
export const PASCAL_TO_CANONICAL: Record<string, string> = {
|
|
57
|
+
// File system operations
|
|
58
|
+
Read: 'read',
|
|
59
|
+
Write: 'write',
|
|
60
|
+
Ls: 'ls',
|
|
61
|
+
Tree: 'tree',
|
|
62
|
+
Cd: 'cd',
|
|
63
|
+
Pwd: 'pwd',
|
|
64
|
+
|
|
65
|
+
// Search operations
|
|
66
|
+
Glob: 'glob',
|
|
67
|
+
Grep: 'ripgrep', // Maps back to ripgrep (primary search tool)
|
|
68
|
+
|
|
69
|
+
// Execution
|
|
70
|
+
Bash: 'bash',
|
|
71
|
+
Terminal: 'terminal',
|
|
72
|
+
|
|
73
|
+
// Git operations
|
|
74
|
+
GitStatus: 'git_status',
|
|
75
|
+
GitDiff: 'git_diff',
|
|
76
|
+
GitCommit: 'git_commit',
|
|
77
|
+
|
|
78
|
+
// Patch/edit
|
|
79
|
+
ApplyPatch: 'apply_patch',
|
|
80
|
+
|
|
81
|
+
// Task management
|
|
82
|
+
UpdateTodos: 'update_todos',
|
|
83
|
+
ProgressUpdate: 'progress_update',
|
|
84
|
+
Finish: 'finish',
|
|
85
|
+
|
|
86
|
+
// Web operations
|
|
87
|
+
WebSearch: 'websearch',
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Convert a canonical tool name to PascalCase format.
|
|
92
|
+
*/
|
|
93
|
+
export function toClaudeCodeName(canonical: string): string {
|
|
94
|
+
if (CANONICAL_TO_PASCAL[canonical]) {
|
|
95
|
+
return CANONICAL_TO_PASCAL[canonical];
|
|
96
|
+
}
|
|
97
|
+
// Default: convert snake_case to PascalCase
|
|
98
|
+
return canonical
|
|
99
|
+
.split('_')
|
|
100
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
101
|
+
.join('');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Convert a PascalCase tool name to canonical format.
|
|
106
|
+
*/
|
|
107
|
+
export function toCanonicalName(pascalCase: string): string {
|
|
108
|
+
if (PASCAL_TO_CANONICAL[pascalCase]) {
|
|
109
|
+
return PASCAL_TO_CANONICAL[pascalCase];
|
|
110
|
+
}
|
|
111
|
+
// Default: convert PascalCase to snake_case
|
|
112
|
+
return pascalCase
|
|
113
|
+
.replace(/([A-Z])/g, '_$1')
|
|
114
|
+
.toLowerCase()
|
|
115
|
+
.replace(/^_/, '');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Check if the current provider/auth combo requires PascalCase naming.
|
|
120
|
+
*/
|
|
121
|
+
export function requiresClaudeCodeNaming(
|
|
122
|
+
provider: string,
|
|
123
|
+
authType?: string,
|
|
124
|
+
): boolean {
|
|
125
|
+
return provider === 'anthropic' && authType === 'oauth';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Transform a tool definition for Claude Code OAuth.
|
|
130
|
+
* Returns a new object with the transformed name.
|
|
131
|
+
*/
|
|
132
|
+
export function transformToolForClaudeCode<T extends { name: string }>(
|
|
133
|
+
tool: T,
|
|
134
|
+
): T {
|
|
135
|
+
return {
|
|
136
|
+
...tool,
|
|
137
|
+
name: toClaudeCodeName(tool.name),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Transform tool call arguments to canonical names.
|
|
143
|
+
* Used when receiving tool calls from Claude Code OAuth.
|
|
144
|
+
*/
|
|
145
|
+
export function normalizeToolCall<T extends { name: string }>(
|
|
146
|
+
call: T,
|
|
147
|
+
fromClaudeCode: boolean,
|
|
148
|
+
): T {
|
|
149
|
+
if (!fromClaudeCode) return call;
|
|
150
|
+
return {
|
|
151
|
+
...call,
|
|
152
|
+
name: toCanonicalName(call.name),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { getDb } from '@ottocode/database';
|
|
2
|
+
import { time } from '../debug/index.ts';
|
|
3
|
+
import type { ToolAdapterContext } from '../../tools/adapter.ts';
|
|
4
|
+
import type { RunOpts } from '../session/queue.ts';
|
|
5
|
+
|
|
6
|
+
export type RunnerToolContext = ToolAdapterContext & { stepIndex: number };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sets up the shared tool context for a run, including the index counter
|
|
10
|
+
* and first tool call tracking.
|
|
11
|
+
*/
|
|
12
|
+
export async function setupToolContext(
|
|
13
|
+
opts: RunOpts,
|
|
14
|
+
db: Awaited<ReturnType<typeof getDb>>,
|
|
15
|
+
) {
|
|
16
|
+
const firstToolTimer = time('runner:first-tool-call');
|
|
17
|
+
let firstToolSeen = false;
|
|
18
|
+
|
|
19
|
+
// Simple counter starting at 0 - first event gets 0, second gets 1, etc.
|
|
20
|
+
let currentIndex = 0;
|
|
21
|
+
const nextIndex = () => currentIndex++;
|
|
22
|
+
|
|
23
|
+
const sharedCtx: RunnerToolContext = {
|
|
24
|
+
nextIndex,
|
|
25
|
+
stepIndex: 0,
|
|
26
|
+
sessionId: opts.sessionId,
|
|
27
|
+
messageId: opts.assistantMessageId,
|
|
28
|
+
assistantPartId: '', // Will be set by runner when first text part is created
|
|
29
|
+
db,
|
|
30
|
+
agent: opts.agent,
|
|
31
|
+
provider: opts.provider,
|
|
32
|
+
model: opts.model,
|
|
33
|
+
projectRoot: opts.projectRoot,
|
|
34
|
+
stepExecution: { states: new Map() },
|
|
35
|
+
toolApprovalMode: opts.toolApprovalMode,
|
|
36
|
+
onFirstToolCall: () => {
|
|
37
|
+
if (firstToolSeen) return;
|
|
38
|
+
firstToolSeen = true;
|
|
39
|
+
firstToolTimer.end();
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return { sharedCtx, firstToolTimer, firstToolSeen: () => firstToolSeen };
|
|
44
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { logger } from '@ottocode/sdk';
|
|
2
|
+
|
|
3
|
+
export type TopupMethod = 'crypto' | 'fiat';
|
|
4
|
+
|
|
5
|
+
export interface PendingTopup {
|
|
6
|
+
sessionId: string;
|
|
7
|
+
messageId: string;
|
|
8
|
+
amountUsd: number;
|
|
9
|
+
currentBalance: number;
|
|
10
|
+
resolve: (method: TopupMethod) => void;
|
|
11
|
+
reject: (error: Error) => void;
|
|
12
|
+
createdAt: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const TOPUP_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
16
|
+
|
|
17
|
+
const pendingTopups = new Map<string, PendingTopup>();
|
|
18
|
+
const timeoutIds = new Map<string, ReturnType<typeof setTimeout>>();
|
|
19
|
+
|
|
20
|
+
export function waitForTopupMethodSelection(
|
|
21
|
+
sessionId: string,
|
|
22
|
+
messageId: string,
|
|
23
|
+
amountUsd: number,
|
|
24
|
+
currentBalance: number,
|
|
25
|
+
): Promise<TopupMethod> {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const existing = pendingTopups.get(sessionId);
|
|
28
|
+
if (existing) {
|
|
29
|
+
existing.reject(new Error('Superseded by new topup request'));
|
|
30
|
+
clearPendingTopup(sessionId);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const pending: PendingTopup = {
|
|
34
|
+
sessionId,
|
|
35
|
+
messageId,
|
|
36
|
+
amountUsd,
|
|
37
|
+
currentBalance,
|
|
38
|
+
resolve,
|
|
39
|
+
reject,
|
|
40
|
+
createdAt: Date.now(),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
pendingTopups.set(sessionId, pending);
|
|
44
|
+
|
|
45
|
+
const timeoutId = setTimeout(() => {
|
|
46
|
+
const p = pendingTopups.get(sessionId);
|
|
47
|
+
if (p) {
|
|
48
|
+
logger.warn(`Topup selection timeout for session ${sessionId}`);
|
|
49
|
+
p.reject(new Error('Topup selection timeout'));
|
|
50
|
+
clearPendingTopup(sessionId);
|
|
51
|
+
}
|
|
52
|
+
}, TOPUP_TIMEOUT_MS);
|
|
53
|
+
|
|
54
|
+
timeoutIds.set(sessionId, timeoutId);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function resolveTopupMethodSelection(
|
|
59
|
+
sessionId: string,
|
|
60
|
+
method: TopupMethod,
|
|
61
|
+
): boolean {
|
|
62
|
+
const pending = pendingTopups.get(sessionId);
|
|
63
|
+
if (!pending) {
|
|
64
|
+
logger.warn(
|
|
65
|
+
`No pending topup found for session ${sessionId} when trying to resolve`,
|
|
66
|
+
);
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
pending.resolve(method);
|
|
71
|
+
clearPendingTopup(sessionId);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function rejectTopupSelection(
|
|
76
|
+
sessionId: string,
|
|
77
|
+
reason: string,
|
|
78
|
+
): boolean {
|
|
79
|
+
const pending = pendingTopups.get(sessionId);
|
|
80
|
+
if (!pending) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
pending.reject(new Error(reason));
|
|
85
|
+
clearPendingTopup(sessionId);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function getPendingTopup(sessionId: string): PendingTopup | undefined {
|
|
90
|
+
return pendingTopups.get(sessionId);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function hasPendingTopup(sessionId: string): boolean {
|
|
94
|
+
return pendingTopups.has(sessionId);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function clearPendingTopup(sessionId: string): void {
|
|
98
|
+
const timeoutId = timeoutIds.get(sessionId);
|
|
99
|
+
if (timeoutId) {
|
|
100
|
+
clearTimeout(timeoutId);
|
|
101
|
+
timeoutIds.delete(sessionId);
|
|
102
|
+
}
|
|
103
|
+
pendingTopups.delete(sessionId);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function clearAllPendingTopups(): void {
|
|
107
|
+
for (const sessionId of pendingTopups.keys()) {
|
|
108
|
+
clearPendingTopup(sessionId);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const cwdMap = new Map<string, string>();
|
|
2
|
+
|
|
3
|
+
export function getCwd(sessionId: string): string {
|
|
4
|
+
return cwdMap.get(sessionId) ?? '.';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function setCwd(sessionId: string, cwd: string) {
|
|
8
|
+
cwdMap.set(sessionId, cwd || '.');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// normalize relative path like './a/../b' -> 'b', never escapes above '.'
|
|
12
|
+
export function normalizeRelative(path: string): string {
|
|
13
|
+
const parts = path.replace(/\\/g, '/').split('/');
|
|
14
|
+
const stack: string[] = [];
|
|
15
|
+
for (const part of parts) {
|
|
16
|
+
if (!part || part === '.') continue;
|
|
17
|
+
if (part === '..') {
|
|
18
|
+
if (stack.length > 0) stack.pop();
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
stack.push(part);
|
|
22
|
+
}
|
|
23
|
+
return stack.length ? stack.join('/') : '.';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function joinRelative(base: string, p: string): string {
|
|
27
|
+
if (!p || p === '.') return base || '.';
|
|
28
|
+
|
|
29
|
+
// Expand tilde to home directory if present
|
|
30
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
31
|
+
if (p === '~' && home) p = home;
|
|
32
|
+
else if (p.startsWith('~/') && home) p = `${home}/${p.slice(2)}`;
|
|
33
|
+
|
|
34
|
+
// If target is absolute, preserve it as absolute
|
|
35
|
+
if (p.startsWith('/')) {
|
|
36
|
+
// Canonicalize: collapse //, resolve . and .. without escaping above root
|
|
37
|
+
const parts = p.replace(/\\/g, '/').split('/');
|
|
38
|
+
const stack: string[] = [];
|
|
39
|
+
for (const part of parts) {
|
|
40
|
+
if (!part || part === '.') continue;
|
|
41
|
+
if (part === '..') {
|
|
42
|
+
if (stack.length > 0) stack.pop();
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
stack.push(part);
|
|
46
|
+
}
|
|
47
|
+
return `/${stack.join('/')}` || '/';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// If base is absolute, join and return absolute
|
|
51
|
+
const baseIsAbs = Boolean(base) && base.startsWith('/');
|
|
52
|
+
if (baseIsAbs) {
|
|
53
|
+
const joined = `${base.replace(/\/$/, '')}/${p}`;
|
|
54
|
+
const parts = joined.replace(/\\/g, '/').split('/');
|
|
55
|
+
const stack: string[] = [];
|
|
56
|
+
for (const part of parts) {
|
|
57
|
+
if (!part || part === '.') continue;
|
|
58
|
+
if (part === '..') {
|
|
59
|
+
if (stack.length > 0) stack.pop();
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
stack.push(part);
|
|
63
|
+
}
|
|
64
|
+
return `/${stack.join('/')}` || '/';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Relative -> Relative join
|
|
68
|
+
return normalizeRelative(`${base || '.'}/${p}`);
|
|
69
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { catalog } from '@ottocode/sdk';
|
|
2
|
+
import { debugLog } from '../debug/index.ts';
|
|
3
|
+
import type { ProviderName } from '../provider/index.ts';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Gets the maximum output tokens allowed for a given provider/model combination.
|
|
7
|
+
* Returns undefined if the information is not available in the catalog.
|
|
8
|
+
*/
|
|
9
|
+
export function getMaxOutputTokens(
|
|
10
|
+
provider: ProviderName,
|
|
11
|
+
modelId: string,
|
|
12
|
+
): number | undefined {
|
|
13
|
+
try {
|
|
14
|
+
const providerCatalog = catalog[provider];
|
|
15
|
+
if (!providerCatalog) {
|
|
16
|
+
debugLog(`[maxOutputTokens] No catalog found for provider: ${provider}`);
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
const modelInfo = providerCatalog.models.find((m) => m.id === modelId);
|
|
20
|
+
if (!modelInfo) {
|
|
21
|
+
debugLog(
|
|
22
|
+
`[maxOutputTokens] No model info found for: ${modelId} in provider: ${provider}`,
|
|
23
|
+
);
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
const outputLimit = modelInfo.limit?.output;
|
|
27
|
+
debugLog(
|
|
28
|
+
`[maxOutputTokens] Provider: ${provider}, Model: ${modelId}, Limit: ${outputLimit}`,
|
|
29
|
+
);
|
|
30
|
+
return outputLimit;
|
|
31
|
+
} catch (err) {
|
|
32
|
+
debugLog(`[maxOutputTokens] Error looking up limit: ${err}`);
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|