@agi-cli/server 0.1.140 → 0.1.142

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agi-cli/server",
3
- "version": "0.1.140",
3
+ "version": "0.1.142",
4
4
  "description": "HTTP API server for AGI CLI",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -29,8 +29,8 @@
29
29
  "typecheck": "tsc --noEmit"
30
30
  },
31
31
  "dependencies": {
32
- "@agi-cli/sdk": "0.1.140",
33
- "@agi-cli/database": "0.1.140",
32
+ "@agi-cli/sdk": "0.1.142",
33
+ "@agi-cli/database": "0.1.142",
34
34
  "drizzle-orm": "^0.44.5",
35
35
  "hono": "^4.9.9",
36
36
  "zod": "^4.1.8"
@@ -1,8 +1,10 @@
1
1
  export type AGIEventType =
2
- | 'solforge.payment.required'
3
- | 'solforge.payment.signing'
4
- | 'solforge.payment.complete'
5
- | 'solforge.payment.error'
2
+ | 'tool.approval.required'
3
+ | 'tool.approval.resolved'
4
+ | 'setu.payment.required'
5
+ | 'setu.payment.signing'
6
+ | 'setu.payment.complete'
7
+ | 'setu.payment.error'
6
8
  | 'session.created'
7
9
  | 'session.updated'
8
10
  | 'message.created'
package/src/index.ts CHANGED
@@ -16,7 +16,8 @@ import { registerTerminalsRoutes } from './routes/terminals.ts';
16
16
  import { registerSessionFilesRoutes } from './routes/session-files.ts';
17
17
  import { registerBranchRoutes } from './routes/branch.ts';
18
18
  import { registerResearchRoutes } from './routes/research.ts';
19
- import { registerSolforgeRoutes } from './routes/solforge.ts';
19
+ import { registerSessionApprovalRoute } from './routes/session-approval.ts';
20
+ import { registerSetuRoutes } from './routes/setu.ts';
20
21
  import type { AgentConfigEntry } from './runtime/agent/registry.ts';
21
22
 
22
23
  const globalTerminalManager = new TerminalManager();
@@ -59,6 +60,7 @@ function initApp() {
59
60
  registerRootRoutes(app);
60
61
  registerOpenApiRoute(app);
61
62
  registerSessionsRoutes(app);
63
+ registerSessionApprovalRoute(app);
62
64
  registerSessionMessagesRoutes(app);
63
65
  registerSessionStreamRoute(app);
64
66
  registerAskRoutes(app);
@@ -69,7 +71,7 @@ function initApp() {
69
71
  registerSessionFilesRoutes(app);
70
72
  registerBranchRoutes(app);
71
73
  registerResearchRoutes(app);
72
- registerSolforgeRoutes(app);
74
+ registerSetuRoutes(app);
73
75
 
74
76
  return app;
75
77
  }
@@ -128,6 +130,7 @@ export function createStandaloneApp(_config?: StandaloneAppConfig) {
128
130
  registerRootRoutes(honoApp);
129
131
  registerOpenApiRoute(honoApp);
130
132
  registerSessionsRoutes(honoApp);
133
+ registerSessionApprovalRoute(honoApp);
131
134
  registerSessionMessagesRoutes(honoApp);
132
135
  registerSessionStreamRoute(honoApp);
133
136
  registerAskRoutes(honoApp);
@@ -138,7 +141,7 @@ export function createStandaloneApp(_config?: StandaloneAppConfig) {
138
141
  registerSessionFilesRoutes(honoApp);
139
142
  registerBranchRoutes(honoApp);
140
143
  registerResearchRoutes(honoApp);
141
- registerSolforgeRoutes(honoApp);
144
+ registerSetuRoutes(honoApp);
142
145
 
143
146
  return honoApp;
144
147
  }
@@ -225,6 +228,7 @@ export function createEmbeddedApp(config: EmbeddedAppConfig = {}) {
225
228
  registerRootRoutes(honoApp);
226
229
  registerOpenApiRoute(honoApp);
227
230
  registerSessionsRoutes(honoApp);
231
+ registerSessionApprovalRoute(honoApp);
228
232
  registerSessionMessagesRoutes(honoApp);
229
233
  registerSessionStreamRoute(honoApp);
230
234
  registerAskRoutes(honoApp);
@@ -235,7 +239,7 @@ export function createEmbeddedApp(config: EmbeddedAppConfig = {}) {
235
239
  registerSessionFilesRoutes(honoApp);
236
240
  registerBranchRoutes(honoApp);
237
241
  registerResearchRoutes(honoApp);
238
- registerSolforgeRoutes(honoApp);
242
+ registerSetuRoutes(honoApp);
239
243
 
240
244
  return honoApp;
241
245
  }
@@ -1,9 +1,9 @@
1
- export const solforgePaths = {
2
- '/v1/solforge/balance': {
1
+ export const setuPaths = {
2
+ '/v1/setu/balance': {
3
3
  get: {
4
- tags: ['solforge'],
5
- operationId: 'getSolforgeBalance',
6
- summary: 'Get Solforge account balance',
4
+ tags: ['setu'],
5
+ operationId: 'getSetuBalance',
6
+ summary: 'Get Setu account balance',
7
7
  description:
8
8
  'Returns wallet balance, total spent, total topups, and request count',
9
9
  responses: {
@@ -44,7 +44,7 @@ export const solforgePaths = {
44
44
  },
45
45
  },
46
46
  502: {
47
- description: 'Failed to fetch balance from Solforge',
47
+ description: 'Failed to fetch balance from Setu',
48
48
  content: {
49
49
  'application/json': {
50
50
  schema: {
@@ -58,11 +58,11 @@ export const solforgePaths = {
58
58
  },
59
59
  },
60
60
  },
61
- '/v1/solforge/wallet': {
61
+ '/v1/setu/wallet': {
62
62
  get: {
63
- tags: ['solforge'],
64
- operationId: 'getSolforgeWallet',
65
- summary: 'Get Solforge wallet info',
63
+ tags: ['setu'],
64
+ operationId: 'getSetuWallet',
65
+ summary: 'Get Setu wallet info',
66
66
  description:
67
67
  'Returns whether the wallet is configured and its public key',
68
68
  responses: {
@@ -85,10 +85,10 @@ export const solforgePaths = {
85
85
  },
86
86
  },
87
87
  },
88
- '/v1/solforge/usdc-balance': {
88
+ '/v1/setu/usdc-balance': {
89
89
  get: {
90
- tags: ['solforge'],
91
- operationId: 'getSolforgeUsdcBalance',
90
+ tags: ['setu'],
91
+ operationId: 'getSetuUsdcBalance',
92
92
  summary: 'Get USDC token balance',
93
93
  description:
94
94
  'Fetches USDC balance from Solana blockchain for the configured wallet',
@@ -8,7 +8,7 @@ import { streamPaths } from './paths/stream';
8
8
  import { schemas } from './schemas';
9
9
 
10
10
  import { terminalsPath } from './paths/terminals';
11
- import { solforgePaths } from './paths/solforge';
11
+ import { setuPaths } from './paths/setu';
12
12
 
13
13
  export function getOpenAPISpec() {
14
14
  const spec = {
@@ -28,7 +28,7 @@ export function getOpenAPISpec() {
28
28
  { name: 'files' },
29
29
  { name: 'git' },
30
30
  { name: 'terminals' },
31
- { name: 'solforge' },
31
+ { name: 'setu' },
32
32
  ],
33
33
  paths: {
34
34
  ...askPaths,
@@ -39,7 +39,7 @@ export function getOpenAPISpec() {
39
39
  ...filesPaths,
40
40
  ...gitPaths,
41
41
  ...terminalsPath,
42
- ...solforgePaths,
42
+ ...setuPaths,
43
43
  },
44
44
  components: {
45
45
  schemas,
@@ -11,6 +11,7 @@ export function registerDefaultsRoute(app: Hono) {
11
11
  agent?: string;
12
12
  provider?: string;
13
13
  model?: string;
14
+ toolApproval?: 'auto' | 'dangerous' | 'all';
14
15
  scope?: 'global' | 'local';
15
16
  }>();
16
17
 
@@ -19,11 +20,13 @@ export function registerDefaultsRoute(app: Hono) {
19
20
  agent: string;
20
21
  provider: string;
21
22
  model: string;
23
+ toolApproval: 'auto' | 'dangerous' | 'all';
22
24
  }> = {};
23
25
 
24
26
  if (body.agent) updates.agent = body.agent;
25
27
  if (body.provider) updates.provider = body.provider;
26
28
  if (body.model) updates.model = body.model;
29
+ if (body.toolApproval) updates.toolApproval = body.toolApproval;
27
30
 
28
31
  await setConfig(scope, updates, projectRoot);
29
32
 
@@ -52,6 +52,11 @@ export function registerMainConfigRoute(app: Hono) {
52
52
  embeddedConfig?.defaults?.model,
53
53
  cfg.defaults.model,
54
54
  ),
55
+ toolApproval: getDefault(
56
+ undefined,
57
+ embeddedConfig?.defaults?.toolApproval,
58
+ cfg.defaults.toolApproval,
59
+ ) as 'auto' | 'dangerous' | 'all',
55
60
  };
56
61
 
57
62
  return c.json({
@@ -4,7 +4,7 @@ import { getDb } from '@agi-cli/database';
4
4
  import { sessions, messages, messageParts } from '@agi-cli/database/schema';
5
5
  import { desc, eq, and, asc, count } from 'drizzle-orm';
6
6
  import type { ProviderId } from '@agi-cli/sdk';
7
- import { isProviderId, catalog } from '@agi-cli/sdk';
7
+ import { isProviderId } from '@agi-cli/sdk';
8
8
  import { serializeError } from '../runtime/errors/api-error.ts';
9
9
  import { logger } from '@agi-cli/sdk';
10
10
  import { publish } from '../events/bus.ts';
@@ -207,7 +207,7 @@ export function registerResearchRoutes(app: Hono) {
207
207
  return c.json({ error: 'Research session not found' }, 404);
208
208
  }
209
209
 
210
- const researchSession = researchRows[0];
210
+ const _researchSession = researchRows[0];
211
211
 
212
212
  const researchMessages = await db
213
213
  .select({
@@ -0,0 +1,53 @@
1
+ import type { Hono } from 'hono';
2
+ import {
3
+ resolveApproval,
4
+ getPendingApprovalsForSession,
5
+ } from '../runtime/tools/approval.ts';
6
+
7
+ export function registerSessionApprovalRoute(app: Hono) {
8
+ app.post('/v1/sessions/:id/approval', async (c) => {
9
+ const sessionId = c.req.param('id');
10
+ const body = await c.req.json<{
11
+ callId: string;
12
+ approved: boolean;
13
+ }>();
14
+
15
+ if (!body.callId) {
16
+ return c.json({ ok: false, error: 'callId is required' }, 400);
17
+ }
18
+
19
+ if (typeof body.approved !== 'boolean') {
20
+ return c.json({ ok: false, error: 'approved must be a boolean' }, 400);
21
+ }
22
+
23
+ console.log('[approval-route] Received approval request', {
24
+ sessionId,
25
+ callId: body.callId,
26
+ approved: body.approved,
27
+ });
28
+
29
+ const result = resolveApproval(body.callId, body.approved);
30
+
31
+ if (!result.ok) {
32
+ return c.json(result, 404);
33
+ }
34
+
35
+ return c.json({ ok: true, callId: body.callId, approved: body.approved });
36
+ });
37
+
38
+ app.get('/v1/sessions/:id/approval/pending', async (c) => {
39
+ const sessionId = c.req.param('id');
40
+ const pending = getPendingApprovalsForSession(sessionId);
41
+
42
+ return c.json({
43
+ ok: true,
44
+ pending: pending.map((p) => ({
45
+ callId: p.callId,
46
+ toolName: p.toolName,
47
+ args: p.args,
48
+ messageId: p.messageId,
49
+ createdAt: p.createdAt,
50
+ })),
51
+ });
52
+ });
53
+ }
@@ -1,6 +1,6 @@
1
1
  import type { Hono } from 'hono';
2
2
  import {
3
- fetchSolforgeBalance,
3
+ fetchSetuBalance,
4
4
  getPublicKeyFromPrivate,
5
5
  getAuth,
6
6
  loadConfig,
@@ -9,14 +9,14 @@ import {
9
9
  import { logger } from '@agi-cli/sdk';
10
10
  import { serializeError } from '../runtime/errors/api-error.ts';
11
11
 
12
- async function getSolforgePrivateKey(): Promise<string | null> {
13
- if (process.env.SOLFORGE_PRIVATE_KEY) {
14
- return process.env.SOLFORGE_PRIVATE_KEY;
12
+ async function getSetuPrivateKey(): Promise<string | null> {
13
+ if (process.env.SETU_PRIVATE_KEY) {
14
+ return process.env.SETU_PRIVATE_KEY;
15
15
  }
16
16
 
17
17
  try {
18
18
  const cfg = await loadConfig(process.cwd());
19
- const auth = await getAuth('solforge', cfg.projectRoot);
19
+ const auth = await getAuth('setu', cfg.projectRoot);
20
20
  if (auth?.type === 'wallet' && auth.secret) {
21
21
  return auth.secret;
22
22
  }
@@ -25,33 +25,33 @@ async function getSolforgePrivateKey(): Promise<string | null> {
25
25
  return null;
26
26
  }
27
27
 
28
- export function registerSolforgeRoutes(app: Hono) {
29
- app.get('/v1/solforge/balance', async (c) => {
28
+ export function registerSetuRoutes(app: Hono) {
29
+ app.get('/v1/setu/balance', async (c) => {
30
30
  try {
31
- const privateKey = await getSolforgePrivateKey();
31
+ const privateKey = await getSetuPrivateKey();
32
32
  if (!privateKey) {
33
- return c.json({ error: 'Solforge wallet not configured' }, 401);
33
+ return c.json({ error: 'Setu wallet not configured' }, 401);
34
34
  }
35
35
 
36
- const balance = await fetchSolforgeBalance({ privateKey });
36
+ const balance = await fetchSetuBalance({ privateKey });
37
37
  if (!balance) {
38
- return c.json({ error: 'Failed to fetch balance from Solforge' }, 502);
38
+ return c.json({ error: 'Failed to fetch balance from Setu' }, 502);
39
39
  }
40
40
 
41
41
  return c.json(balance);
42
42
  } catch (error) {
43
- logger.error('Failed to fetch Solforge balance', error);
43
+ logger.error('Failed to fetch Setu balance', error);
44
44
  const errorResponse = serializeError(error);
45
45
  return c.json(errorResponse, errorResponse.error.status || 500);
46
46
  }
47
47
  });
48
48
 
49
- app.get('/v1/solforge/wallet', async (c) => {
49
+ app.get('/v1/setu/wallet', async (c) => {
50
50
  try {
51
- const privateKey = await getSolforgePrivateKey();
51
+ const privateKey = await getSetuPrivateKey();
52
52
  if (!privateKey) {
53
53
  return c.json(
54
- { error: 'Solforge wallet not configured', configured: false },
54
+ { error: 'Setu wallet not configured', configured: false },
55
55
  200,
56
56
  );
57
57
  }
@@ -66,17 +66,17 @@ export function registerSolforgeRoutes(app: Hono) {
66
66
  publicKey,
67
67
  });
68
68
  } catch (error) {
69
- logger.error('Failed to get Solforge wallet info', error);
69
+ logger.error('Failed to get Setu wallet info', error);
70
70
  const errorResponse = serializeError(error);
71
71
  return c.json(errorResponse, errorResponse.error.status || 500);
72
72
  }
73
73
  });
74
74
 
75
- app.get('/v1/solforge/usdc-balance', async (c) => {
75
+ app.get('/v1/setu/usdc-balance', async (c) => {
76
76
  try {
77
- const privateKey = await getSolforgePrivateKey();
77
+ const privateKey = await getSetuPrivateKey();
78
78
  if (!privateKey) {
79
- return c.json({ error: 'Solforge wallet not configured' }, 401);
79
+ return c.json({ error: 'Setu wallet not configured' }, 401);
80
80
  }
81
81
 
82
82
  const network =
@@ -127,7 +127,7 @@ async function processAskRequest(
127
127
  google: { enabled: true },
128
128
  openrouter: { enabled: true },
129
129
  opencode: { enabled: true },
130
- solforge: { enabled: true },
130
+ setu: { enabled: true },
131
131
  },
132
132
  paths: {
133
133
  dataDir: `${projectRoot}/.agi`,
@@ -62,7 +62,7 @@ export async function performAutoCompaction(
62
62
  | 'google'
63
63
  | 'openrouter'
64
64
  | 'opencode'
65
- | 'solforge'
65
+ | 'setu'
66
66
  | 'zai'
67
67
  | 'zai-coding',
68
68
  cfg.projectRoot,
@@ -171,6 +171,9 @@ export async function dispatchAssistantMessage(
171
171
  );
172
172
  }
173
173
 
174
+ // Read tool approval mode from config
175
+ const toolApprovalMode = cfg.defaults.toolApproval ?? 'auto';
176
+
174
177
  enqueueAssistantRun(
175
178
  {
176
179
  sessionId,
@@ -184,6 +187,7 @@ export async function dispatchAssistantMessage(
184
187
  reasoningText,
185
188
  isCompactCommand: isCompact,
186
189
  compactionContext,
190
+ toolApprovalMode,
187
191
  },
188
192
  runSessionLoop,
189
193
  );
@@ -3,9 +3,10 @@ import { getAnthropicInstance } from './anthropic.ts';
3
3
  import { resolveOpenAIModel } from './openai.ts';
4
4
  import { resolveGoogleModel } from './google.ts';
5
5
  import { resolveOpenRouterModel } from './openrouter.ts';
6
- import { resolveSolforgeModel } from './solforge.ts';
6
+ import { resolveSetuModel } from './setu.ts';
7
7
  import { getZaiInstance, getZaiCodingInstance } from './zai.ts';
8
8
  import { resolveOpencodeModel } from './opencode.ts';
9
+ import { getMoonshotInstance } from './moonshot.ts';
9
10
 
10
11
  export type ProviderName = ProviderId;
11
12
 
@@ -33,8 +34,8 @@ export async function resolveModel(
33
34
  if (provider === 'opencode') {
34
35
  return resolveOpencodeModel(model, cfg);
35
36
  }
36
- if (provider === 'solforge') {
37
- return resolveSolforgeModel(model, options?.sessionId);
37
+ if (provider === 'setu') {
38
+ return resolveSetuModel(model, options?.sessionId);
38
39
  }
39
40
  if (provider === 'zai') {
40
41
  return getZaiInstance(cfg, model);
@@ -42,5 +43,8 @@ export async function resolveModel(
42
43
  if (provider === 'zai-coding') {
43
44
  return getZaiCodingInstance(cfg, model);
44
45
  }
46
+ if (provider === 'moonshot') {
47
+ return getMoonshotInstance(cfg, model);
48
+ }
45
49
  throw new Error(`Unsupported provider: ${provider}`);
46
50
  }
@@ -0,0 +1,8 @@
1
+ import type { AGIConfig } from '@agi-cli/sdk';
2
+ import { getAuth, createMoonshotModel } from '@agi-cli/sdk';
3
+
4
+ export async function getMoonshotInstance(cfg: AGIConfig, model: string) {
5
+ const auth = await getAuth('moonshot', cfg.projectRoot);
6
+ const apiKey = auth?.type === 'api' ? auth.key : undefined;
7
+ return createMoonshotModel(model, { apiKey });
8
+ }
@@ -14,7 +14,7 @@ const FALLBACK_ORDER: ProviderId[] = [
14
14
  'google',
15
15
  'opencode',
16
16
  'openrouter',
17
- 'solforge',
17
+ 'setu',
18
18
  ];
19
19
 
20
20
  type SelectionInput = {
@@ -1,51 +1,51 @@
1
1
  import {
2
- createSolforgeModel,
2
+ createSetuModel,
3
3
  catalog,
4
- type SolforgePaymentCallbacks,
4
+ type SetuPaymentCallbacks,
5
5
  } from '@agi-cli/sdk';
6
6
  import { publish } from '../../events/bus.ts';
7
7
 
8
8
  function getProviderNpm(model: string): string | undefined {
9
- const entry = catalog.solforge?.models?.find((m) => m.id === model);
9
+ const entry = catalog.setu?.models?.find((m) => m.id === model);
10
10
  return entry?.provider?.npm;
11
11
  }
12
12
 
13
- export function resolveSolforgeModel(model: string, sessionId?: string) {
14
- const privateKey = process.env.SOLFORGE_PRIVATE_KEY ?? '';
13
+ export function resolveSetuModel(model: string, sessionId?: string) {
14
+ const privateKey = process.env.SETU_PRIVATE_KEY ?? '';
15
15
  if (!privateKey) {
16
16
  throw new Error(
17
- 'Solforge provider requires SOLFORGE_PRIVATE_KEY (base58 Solana secret).',
17
+ 'Setu provider requires SETU_PRIVATE_KEY (base58 Solana secret).',
18
18
  );
19
19
  }
20
- const baseURL = process.env.SOLFORGE_BASE_URL;
21
- const rpcURL = process.env.SOLFORGE_SOLANA_RPC_URL;
20
+ const baseURL = process.env.SETU_BASE_URL;
21
+ const rpcURL = process.env.SETU_SOLANA_RPC_URL;
22
22
 
23
- const callbacks: SolforgePaymentCallbacks = sessionId
23
+ const callbacks: SetuPaymentCallbacks = sessionId
24
24
  ? {
25
25
  onPaymentRequired: (amountUsd) => {
26
26
  publish({
27
- type: 'solforge.payment.required',
27
+ type: 'setu.payment.required',
28
28
  sessionId,
29
29
  payload: { amountUsd },
30
30
  });
31
31
  },
32
32
  onPaymentSigning: () => {
33
33
  publish({
34
- type: 'solforge.payment.signing',
34
+ type: 'setu.payment.signing',
35
35
  sessionId,
36
36
  payload: {},
37
37
  });
38
38
  },
39
39
  onPaymentComplete: (data) => {
40
40
  publish({
41
- type: 'solforge.payment.complete',
41
+ type: 'setu.payment.complete',
42
42
  sessionId,
43
43
  payload: data,
44
44
  });
45
45
  },
46
46
  onPaymentError: (error) => {
47
47
  publish({
48
- type: 'solforge.payment.error',
48
+ type: 'setu.payment.error',
49
49
  sessionId,
50
50
  payload: { error },
51
51
  });
@@ -55,7 +55,7 @@ export function resolveSolforgeModel(model: string, sessionId?: string) {
55
55
 
56
56
  const providerNpm = getProviderNpm(model);
57
57
 
58
- return createSolforgeModel(
58
+ return createSetuModel(
59
59
  model,
60
60
  { privateKey },
61
61
  {
@@ -74,7 +74,7 @@ export function resolveUsageProvider(
74
74
  model: string,
75
75
  ): ProviderId {
76
76
  if (
77
- provider !== 'solforge' &&
77
+ provider !== 'setu' &&
78
78
  provider !== 'openrouter' &&
79
79
  provider !== 'opencode'
80
80
  ) {
@@ -1,5 +1,6 @@
1
1
  import type { ProviderName } from '../provider/index.ts';
2
2
  import { publish } from '../../events/bus.ts';
3
+ import type { ToolApprovalMode } from '../tools/approval.ts';
3
4
 
4
5
  export type RunOpts = {
5
6
  sessionId: string;
@@ -14,6 +15,7 @@ export type RunOpts = {
14
15
  abortSignal?: AbortSignal;
15
16
  isCompactCommand?: boolean;
16
17
  compactionContext?: string;
18
+ toolApprovalMode?: ToolApprovalMode;
17
19
  };
18
20
 
19
21
  export type QueuedMessage = {
@@ -8,7 +8,6 @@ import {
8
8
  pruneSession,
9
9
  isOverflow,
10
10
  getModelLimits,
11
- type TokenUsage,
12
11
  markSessionCompacted,
13
12
  } from '../message/compaction.ts';
14
13
  import { debugLog } from '../debug/index.ts';
@@ -47,7 +47,7 @@ export function createStepFinishHandler(
47
47
  try {
48
48
  await updateSessionTokensIncrementalFn(
49
49
  step.usage,
50
- step.experimental_providerMetadata,
50
+ step.providerMetadata,
51
51
  opts,
52
52
  db,
53
53
  );
@@ -56,7 +56,7 @@ export function createStepFinishHandler(
56
56
  try {
57
57
  await updateMessageTokensIncrementalFn(
58
58
  step.usage,
59
- step.experimental_providerMetadata,
59
+ step.providerMetadata,
60
60
  opts,
61
61
  db,
62
62
  );
@@ -4,7 +4,7 @@ export type StepFinishEvent = {
4
4
  usage?: UsageData;
5
5
  finishReason?: string;
6
6
  response?: unknown;
7
- experimental_providerMetadata?: ProviderMetadata;
7
+ providerMetadata?: ProviderMetadata;
8
8
  };
9
9
 
10
10
  export type FinishEvent = {
@@ -0,0 +1,179 @@
1
+ import { publish } from '../../events/bus.ts';
2
+
3
+ export type ToolApprovalMode = 'auto' | 'dangerous' | 'all';
4
+
5
+ export const DANGEROUS_TOOLS = new Set([
6
+ 'bash',
7
+ 'write',
8
+ 'apply_patch',
9
+ 'terminal',
10
+ 'edit',
11
+ 'git_commit',
12
+ 'git_push',
13
+ ]);
14
+
15
+ export const SAFE_TOOLS = new Set([
16
+ 'finish',
17
+ 'progress_update',
18
+ 'update_todos',
19
+ ]);
20
+
21
+ export interface PendingApproval {
22
+ callId: string;
23
+ toolName: string;
24
+ args: unknown;
25
+ sessionId: string;
26
+ messageId: string;
27
+ resolve: (approved: boolean) => void;
28
+ createdAt: number;
29
+ }
30
+
31
+ const pendingApprovals = new Map<string, PendingApproval>();
32
+
33
+ export function requiresApproval(
34
+ toolName: string,
35
+ mode: ToolApprovalMode,
36
+ ): boolean {
37
+ if (SAFE_TOOLS.has(toolName)) return false;
38
+ if (mode === 'auto') return false;
39
+ if (mode === 'all') return true;
40
+ if (mode === 'dangerous') return DANGEROUS_TOOLS.has(toolName);
41
+ return false;
42
+ }
43
+
44
+ export async function requestApproval(
45
+ sessionId: string,
46
+ messageId: string,
47
+ callId: string,
48
+ toolName: string,
49
+ args: unknown,
50
+ timeoutMs = 120000,
51
+ ): Promise<boolean> {
52
+ console.log('[approval] requestApproval called', {
53
+ sessionId,
54
+ messageId,
55
+ callId,
56
+ toolName,
57
+ });
58
+ return new Promise((resolve) => {
59
+ const approval: PendingApproval = {
60
+ callId,
61
+ toolName,
62
+ args,
63
+ sessionId,
64
+ messageId,
65
+ resolve,
66
+ createdAt: Date.now(),
67
+ };
68
+
69
+ pendingApprovals.set(callId, approval);
70
+ console.log(
71
+ '[approval] Added to pendingApprovals, count:',
72
+ pendingApprovals.size,
73
+ );
74
+
75
+ publish({
76
+ type: 'tool.approval.required',
77
+ sessionId,
78
+ payload: {
79
+ callId,
80
+ toolName,
81
+ args,
82
+ messageId,
83
+ },
84
+ });
85
+
86
+ setTimeout(() => {
87
+ if (pendingApprovals.has(callId)) {
88
+ pendingApprovals.delete(callId);
89
+ resolve(false);
90
+ publish({
91
+ type: 'tool.approval.resolved',
92
+ sessionId,
93
+ payload: {
94
+ callId,
95
+ toolName,
96
+ approved: false,
97
+ reason: 'timeout',
98
+ },
99
+ });
100
+ }
101
+ }, timeoutMs);
102
+ });
103
+ }
104
+
105
+ export function resolveApproval(
106
+ callId: string,
107
+ approved: boolean,
108
+ ): { ok: boolean; error?: string } {
109
+ console.log('[approval] resolveApproval called', {
110
+ callId,
111
+ approved,
112
+ pendingCount: pendingApprovals.size,
113
+ pendingIds: [...pendingApprovals.keys()],
114
+ });
115
+ const approval = pendingApprovals.get(callId);
116
+ if (!approval) {
117
+ console.log('[approval] No pending approval found for callId:', callId);
118
+ return { ok: false, error: 'No pending approval found for this callId' };
119
+ }
120
+
121
+ pendingApprovals.delete(callId);
122
+ approval.resolve(approved);
123
+
124
+ publish({
125
+ type: 'tool.approval.resolved',
126
+ sessionId: approval.sessionId,
127
+ payload: {
128
+ callId,
129
+ toolName: approval.toolName,
130
+ approved,
131
+ reason: approved ? 'user_approved' : 'user_rejected',
132
+ },
133
+ });
134
+
135
+ return { ok: true };
136
+ }
137
+
138
+ export function getPendingApproval(
139
+ callId: string,
140
+ ): PendingApproval | undefined {
141
+ return pendingApprovals.get(callId);
142
+ }
143
+
144
+ export function updateApprovalArgs(callId: string, args: unknown): boolean {
145
+ const approval = pendingApprovals.get(callId);
146
+ if (!approval) return false;
147
+
148
+ approval.args = args;
149
+
150
+ publish({
151
+ type: 'tool.approval.updated',
152
+ sessionId: approval.sessionId,
153
+ payload: {
154
+ callId,
155
+ toolName: approval.toolName,
156
+ args,
157
+ messageId: approval.messageId,
158
+ },
159
+ });
160
+
161
+ return true;
162
+ }
163
+
164
+ export function getPendingApprovalsForSession(
165
+ sessionId: string,
166
+ ): PendingApproval[] {
167
+ return Array.from(pendingApprovals.values()).filter(
168
+ (a) => a.sessionId === sessionId,
169
+ );
170
+ }
171
+
172
+ export function clearPendingApprovalsForSession(sessionId: string): void {
173
+ for (const [callId, approval] of pendingApprovals) {
174
+ if (approval.sessionId === sessionId) {
175
+ approval.resolve(false);
176
+ pendingApprovals.delete(callId);
177
+ }
178
+ }
179
+ }
@@ -1,6 +1,7 @@
1
1
  import { eq } from 'drizzle-orm';
2
2
  import type { DB } from '@agi-cli/database';
3
3
  import { messageParts } from '@agi-cli/database/schema';
4
+ import type { ToolApprovalMode } from './approval.ts';
4
5
  import { publish } from '../../events/bus.ts';
5
6
 
6
7
  export type StepExecutionState = {
@@ -24,6 +25,7 @@ export type ToolAdapterContext = {
24
25
  stepExecution?: {
25
26
  states: Map<number, StepExecutionState>;
26
27
  };
28
+ toolApprovalMode?: ToolApprovalMode;
27
29
  };
28
30
 
29
31
  export function extractFinishText(input: unknown): string | undefined {
@@ -32,6 +32,7 @@ export async function setupToolContext(
32
32
  model: opts.model,
33
33
  projectRoot: opts.projectRoot,
34
34
  stepExecution: { states: new Map() },
35
+ toolApprovalMode: opts.toolApprovalMode,
35
36
  onFirstToolCall: () => {
36
37
  if (firstToolSeen) return;
37
38
  firstToolSeen = true;
@@ -13,6 +13,10 @@ import {
13
13
  toClaudeCodeName,
14
14
  requiresClaudeCodeNaming,
15
15
  } from '../runtime/tools/mapping.ts';
16
+ import {
17
+ requiresApproval,
18
+ requestApproval,
19
+ } from '../runtime/tools/approval.ts';
16
20
 
17
21
  export type { ToolAdapterContext } from '../runtime/tools/context.ts';
18
22
 
@@ -33,6 +37,7 @@ type PendingCallMeta = {
33
37
  startTs: number;
34
38
  stepIndex?: number;
35
39
  args?: unknown;
40
+ approvalPromise?: Promise<boolean>;
36
41
  };
37
42
 
38
43
  function getPendingQueue(
@@ -294,6 +299,19 @@ export function adaptTools(
294
299
  toolCallId: callId,
295
300
  });
296
301
  } catch {}
302
+ // Start approval request with full args
303
+ if (
304
+ ctx.toolApprovalMode &&
305
+ requiresApproval(name, ctx.toolApprovalMode)
306
+ ) {
307
+ meta.approvalPromise = requestApproval(
308
+ ctx.sessionId,
309
+ ctx.messageId,
310
+ callId,
311
+ name,
312
+ args,
313
+ );
314
+ }
297
315
  if (typeof base.onInputAvailable === 'function') {
298
316
  // biome-ignore lint/suspicious/noExplicitAny: AI SDK types are complex
299
317
  await base.onInputAvailable(options as any);
@@ -324,6 +342,16 @@ export function adaptTools(
324
342
 
325
343
  const executeWithGuards = async (): Promise<ToolExecuteReturn> => {
326
344
  try {
345
+ // Await approval if it was requested in onInputAvailable
346
+ if (meta?.approvalPromise) {
347
+ const approved = await meta.approvalPromise;
348
+ if (!approved) {
349
+ return {
350
+ ok: false,
351
+ error: 'Tool execution rejected by user',
352
+ } as ToolExecuteReturn;
353
+ }
354
+ }
327
355
  // Handle session-relative paths and cwd tools
328
356
  let res: ToolExecuteReturn | { cwd: string } | null | undefined;
329
357
  const cwd = getCwd(ctx.sessionId);
@@ -136,12 +136,12 @@ export function buildGetParentSessionTool(
136
136
  try {
137
137
  const parsed = JSON.parse(part.content);
138
138
  if (parsed?.text) {
139
- textContent += parsed.text + '\n';
139
+ textContent += `${parsed.text}\n`;
140
140
  } else {
141
- textContent += part.content + '\n';
141
+ textContent += `${part.content}\n`;
142
142
  }
143
143
  } catch {
144
- textContent += part.content + '\n';
144
+ textContent += `${part.content}\n`;
145
145
  }
146
146
  }
147
147
  if (part.type === 'tool_call' && part.toolName) {
@@ -2,7 +2,7 @@ import { tool } from 'ai';
2
2
  import { z } from 'zod/v3';
3
3
  import { getDb } from '@agi-cli/database';
4
4
  import { sessions, messages, messageParts } from '@agi-cli/database/schema';
5
- import { eq, desc, asc, gte, lte, and, like, count, sql } from 'drizzle-orm';
5
+ import { eq, desc, asc, gte, lte, and, count, sql } from 'drizzle-orm';
6
6
 
7
7
  const inputSchema = z.object({
8
8
  sessionId: z.string().optional().describe('Filter by specific session ID'),
@@ -47,7 +47,7 @@ export function buildQueryMessagesTool(projectRoot: string) {
47
47
  if (input.sessionId) {
48
48
  conditions.push(eq(messages.sessionId, input.sessionId));
49
49
  } else {
50
- const projectSessions = db
50
+ const _projectSessions = db
51
51
  .select({ id: sessions.id })
52
52
  .from(sessions)
53
53
  .where(eq(sessions.projectPath, projectRoot));
@@ -2,7 +2,7 @@ import { tool } from 'ai';
2
2
  import { z } from 'zod/v3';
3
3
  import { getDb } from '@agi-cli/database';
4
4
  import { sessions, messages } from '@agi-cli/database/schema';
5
- import { eq, desc, asc, gte, lte, and, sql, count } from 'drizzle-orm';
5
+ import { eq, desc, asc, gte, lte, and, count } from 'drizzle-orm';
6
6
 
7
7
  const inputSchema = z.object({
8
8
  limit: z
@@ -2,7 +2,7 @@ import { tool } from 'ai';
2
2
  import { z } from 'zod/v3';
3
3
  import { getDb } from '@agi-cli/database';
4
4
  import { sessions, messages, messageParts } from '@agi-cli/database/schema';
5
- import { eq, desc, asc, like, and, sql } from 'drizzle-orm';
5
+ import { eq, like, and } from 'drizzle-orm';
6
6
 
7
7
  const inputSchema = z.object({
8
8
  query: z.string().min(1).describe('Search term to find in message content'),
package/sst-env.d.ts CHANGED
@@ -2,9 +2,7 @@
2
2
  /* tslint:disable */
3
3
  /* eslint-disable */
4
4
  /* deno-fmt-ignore-file */
5
- /* biome-ignore-all lint: auto-generated */
6
5
 
7
6
  /// <reference path="../../sst-env.d.ts" />
8
7
 
9
8
  import 'sst';
10
- export {};