@agent-api/cli 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import { type AgentResponse, type PresetToolCatalogClient, type ResponseStreamEvent, type Tool } from "@agent-api/sdk";
2
+ import type { ShellIsolationPreferences } from "../workbench/shell-isolation.js";
2
3
  export interface AgentRunOptions {
3
4
  profile?: string;
4
5
  promptParts: string[];
@@ -19,6 +20,7 @@ export interface AgentRunOptions {
19
20
  maxContextFiles?: number;
20
21
  maxContextBytes?: number;
21
22
  accessMode?: WorkdirAccessMode;
23
+ shellIsolation?: ShellIsolationPreferences;
22
24
  abortSignal?: AbortSignal;
23
25
  }
24
26
  export type WorkdirAccessMode = "off" | "approval" | "full";
@@ -4,6 +4,7 @@ import { createLocalShellToolRegistry, createLocalWorkdirToolRegistry, localShel
4
4
  import { resolvePreviousResponseID, updateConversation } from "../conversation/index.js";
5
5
  import { resolveRuntimeProfile } from "../profile.js";
6
6
  import { buildWorkdirContextBlock, openWorkdir } from "../workdir/index.js";
7
+ import { localShellIsolationOptions } from "../workbench/shell-isolation.js";
7
8
  const defaultCatalogCacheTTLMS = 10 * 60_000;
8
9
  const presetCatalogCache = new Map();
9
10
  const toolCatalogCache = new Map();
@@ -381,6 +382,7 @@ async function prepareLocalWorkdirTools(options) {
381
382
  const shellRegistry = createLocalShellToolRegistry({
382
383
  accessMode: localToolAccessMode(options),
383
384
  workdir: service.workdir,
385
+ ...localShellIsolationOptions(options.shellIsolation),
384
386
  });
385
387
  return {
386
388
  registry: combineLocalToolRegistries(registry, shellRegistry),
@@ -389,6 +391,7 @@ async function prepareLocalWorkdirTools(options) {
389
391
  localShellToolInstructions({
390
392
  accessMode: localToolAccessMode(options),
391
393
  cwd: service.workdir.root,
394
+ ...localShellIsolationOptions(options.shellIsolation),
392
395
  }),
393
396
  "Use local_workdir for selected local workdir operations. Prefer summarize/list/search/grep before read/read_lines. Prefer preview_edits/apply_edits for source edits. Use local_shell for command/process tasks. In approval mode, local actions return requires_approval and must be explained to the user instead of retried blindly.",
394
397
  ].join("\n\n"),
package/dist/config.d.ts CHANGED
@@ -1,5 +1,8 @@
1
+ import type { ShellIsolationMode, ShellIsolationPreferences } from "./workbench/shell-isolation.js";
1
2
  export declare const defaultBaseURL = "https://api.agentsway.dev";
2
3
  export declare const configFile = "profiles.json";
4
+ export declare const appConfigurationFile = "configuration.json";
5
+ export declare const conversationsFile = "conversations.json";
3
6
  export type AuthProfile = {
4
7
  type: "api_key";
5
8
  apiKey: string;
@@ -20,8 +23,6 @@ export interface Profile {
20
23
  export interface CLIConfig {
21
24
  activeProfile: string;
22
25
  profiles: Record<string, Profile>;
23
- conversations: Record<string, ConversationState>;
24
- workbench: WorkbenchPreferences;
25
26
  }
26
27
  export interface ConversationState {
27
28
  name: string;
@@ -31,14 +32,35 @@ export interface ConversationState {
31
32
  }
32
33
  export interface WorkbenchPreferences {
33
34
  defaultPreset?: string | null;
35
+ isolation?: ShellIsolationPreferences;
36
+ }
37
+ export interface AppConfiguration {
38
+ workbench: WorkbenchPreferences;
39
+ }
40
+ export interface ConversationConfiguration {
41
+ conversations: Record<string, ConversationState>;
34
42
  }
35
43
  export declare function loadConfig(): Promise<CLIConfig>;
36
44
  export declare function saveConfig(config: CLIConfig): Promise<void>;
45
+ export declare function loadAppConfiguration(): Promise<AppConfiguration>;
46
+ export declare function saveAppConfiguration(config: AppConfiguration): Promise<void>;
47
+ export declare function loadConversationConfiguration(): Promise<ConversationConfiguration>;
48
+ export declare function saveConversationConfiguration(config: ConversationConfiguration): Promise<void>;
37
49
  export declare function upsertProfile(profile: Omit<Profile, "createdAt" | "updatedAt">): Promise<Profile>;
38
50
  export declare function activeProfile(profileName?: string): Promise<Profile>;
39
51
  export declare function emptyConfig(): CLIConfig;
52
+ export declare function emptyAppConfiguration(): AppConfiguration;
53
+ export declare function emptyConversationConfiguration(): ConversationConfiguration;
40
54
  export declare function loadWorkbenchPreferences(): Promise<WorkbenchPreferences>;
41
55
  export declare function updateWorkbenchPreferences(patch: {
42
56
  defaultPreset?: string | null | undefined;
57
+ isolation?: {
58
+ mode?: ShellIsolationMode | null | undefined;
59
+ executablePath?: string | null | undefined;
60
+ version?: string | null | undefined;
61
+ sourceURL?: string | null | undefined;
62
+ sha256?: string | null | undefined;
63
+ installSkipped?: boolean | null | undefined;
64
+ };
43
65
  }): Promise<WorkbenchPreferences>;
44
66
  export declare function redactSecret(value: string): string;
package/dist/config.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { z } from "zod";
2
- import { runtime } from "./runtime/index.js";
2
+ import { ensureRuntime, runtime } from "./runtime/index.js";
3
3
  export const defaultBaseURL = "https://api.agentsway.dev";
4
4
  export const configFile = "profiles.json";
5
+ export const appConfigurationFile = "configuration.json";
6
+ export const conversationsFile = "conversations.json";
5
7
  const authProfileSchema = z.discriminatedUnion("type", [
6
8
  z.object({
7
9
  type: z.literal("api_key"),
@@ -30,15 +32,28 @@ const conversationSchema = z.object({
30
32
  });
31
33
  const workbenchPreferencesSchema = z.object({
32
34
  defaultPreset: z.string().nullable().optional(),
35
+ isolation: z.object({
36
+ mode: z.enum(["none", "auto", "required"]).optional(),
37
+ executablePath: z.string().nullable().optional(),
38
+ version: z.string().nullable().optional(),
39
+ sourceURL: z.string().nullable().optional(),
40
+ sha256: z.string().nullable().optional(),
41
+ installSkipped: z.boolean().nullable().optional(),
42
+ }).optional(),
33
43
  }).default({});
34
44
  const cliConfigSchema = z.object({
35
45
  activeProfile: z.string().default("default"),
36
46
  profiles: z.record(z.string(), profileSchema).default({}),
37
47
  conversations: z.record(z.string(), conversationSchema).default({}),
48
+ });
49
+ const appConfigurationSchema = z.object({
38
50
  workbench: workbenchPreferencesSchema,
39
51
  });
52
+ const conversationConfigurationSchema = z.object({
53
+ conversations: z.record(z.string(), conversationSchema).default({}),
54
+ });
40
55
  export async function loadConfig() {
41
- await runtime.ensure();
56
+ await ensureRuntime();
42
57
  const loaded = await runtime.config.read(configFile, emptyConfig());
43
58
  const parsed = cliConfigSchema.safeParse(loaded);
44
59
  if (!parsed.success) {
@@ -47,8 +62,37 @@ export async function loadConfig() {
47
62
  return parsed.data;
48
63
  }
49
64
  export async function saveConfig(config) {
50
- await runtime.ensure();
51
- await runtime.config.write(configFile, config);
65
+ await ensureRuntime();
66
+ await runtime.config.write(configFile, {
67
+ activeProfile: config.activeProfile,
68
+ profiles: config.profiles,
69
+ });
70
+ }
71
+ export async function loadAppConfiguration() {
72
+ await ensureRuntime();
73
+ const loaded = await runtime.config.read(appConfigurationFile, emptyAppConfiguration());
74
+ const parsed = appConfigurationSchema.safeParse(loaded);
75
+ if (!parsed.success) {
76
+ throw new Error(`Invalid app configuration: ${parsed.error.issues.map((issue) => issue.message).join("; ")}`);
77
+ }
78
+ return parsed.data;
79
+ }
80
+ export async function saveAppConfiguration(config) {
81
+ await ensureRuntime();
82
+ await runtime.config.write(appConfigurationFile, config);
83
+ }
84
+ export async function loadConversationConfiguration() {
85
+ await ensureRuntime();
86
+ const loaded = await runtime.config.read(conversationsFile, emptyConversationConfiguration());
87
+ const parsed = conversationConfigurationSchema.safeParse(loaded);
88
+ if (!parsed.success) {
89
+ throw new Error(`Invalid conversation configuration: ${parsed.error.issues.map((issue) => issue.message).join("; ")}`);
90
+ }
91
+ return parsed.data;
92
+ }
93
+ export async function saveConversationConfiguration(config) {
94
+ await ensureRuntime();
95
+ await runtime.config.write(conversationsFile, config);
52
96
  }
53
97
  export async function upsertProfile(profile) {
54
98
  const config = await loadConfig();
@@ -74,15 +118,21 @@ export async function activeProfile(profileName) {
74
118
  return profile;
75
119
  }
76
120
  export function emptyConfig() {
77
- return { activeProfile: "default", profiles: {}, conversations: {}, workbench: {} };
121
+ return { activeProfile: "default", profiles: {} };
122
+ }
123
+ export function emptyAppConfiguration() {
124
+ return { workbench: {} };
125
+ }
126
+ export function emptyConversationConfiguration() {
127
+ return { conversations: {} };
78
128
  }
79
129
  export async function loadWorkbenchPreferences() {
80
- const config = await loadConfig();
81
- return config.workbench;
130
+ const appConfig = await loadAppConfiguration();
131
+ return appConfig.workbench;
82
132
  }
83
133
  export async function updateWorkbenchPreferences(patch) {
84
- const config = await loadConfig();
85
- const next = { ...config.workbench };
134
+ const appConfig = await loadAppConfiguration();
135
+ const next = { ...appConfig.workbench };
86
136
  if ("defaultPreset" in patch) {
87
137
  if (patch.defaultPreset === undefined) {
88
138
  delete next.defaultPreset;
@@ -100,10 +150,50 @@ export async function updateWorkbenchPreferences(patch) {
100
150
  }
101
151
  }
102
152
  }
103
- config.workbench = next;
104
- await saveConfig(config);
153
+ if ("isolation" in patch) {
154
+ next.isolation = updateIsolationPreferences(next.isolation, patch.isolation);
155
+ }
156
+ appConfig.workbench = next;
157
+ await saveAppConfiguration(appConfig);
105
158
  return next;
106
159
  }
160
+ function updateIsolationPreferences(current, patch) {
161
+ const next = { ...(current ?? {}) };
162
+ if (!patch)
163
+ return Object.keys(next).length > 0 ? next : undefined;
164
+ if ("mode" in patch) {
165
+ if (patch.mode === null || patch.mode === undefined) {
166
+ delete next.mode;
167
+ }
168
+ else {
169
+ next.mode = patch.mode;
170
+ }
171
+ }
172
+ for (const key of ["executablePath", "version", "sourceURL", "sha256"]) {
173
+ if (key in patch) {
174
+ const value = patch[key];
175
+ if (value === undefined || value === null) {
176
+ delete next[key];
177
+ }
178
+ else {
179
+ const trimmed = value.trim();
180
+ if (trimmed)
181
+ next[key] = trimmed;
182
+ else
183
+ delete next[key];
184
+ }
185
+ }
186
+ }
187
+ if ("installSkipped" in patch) {
188
+ if (patch.installSkipped === undefined || patch.installSkipped === null) {
189
+ delete next.installSkipped;
190
+ }
191
+ else {
192
+ next.installSkipped = patch.installSkipped;
193
+ }
194
+ }
195
+ return Object.keys(next).length > 0 ? next : undefined;
196
+ }
107
197
  export function redactSecret(value) {
108
198
  if (value.length <= 10)
109
199
  return "***";
@@ -1,4 +1,4 @@
1
- import { activeProfile, loadConfig, saveConfig } from "../config.js";
1
+ import { activeProfile, loadConversationConfiguration, saveConversationConfiguration } from "../config.js";
2
2
  export function conversationKey(profile, name) {
3
3
  return `${profile}:${name}`;
4
4
  }
@@ -10,32 +10,32 @@ export async function resolvePreviousResponseID(options) {
10
10
  if (!options.continueConversation)
11
11
  return undefined;
12
12
  const profile = await activeProfile(options.profile);
13
- const config = await loadConfig();
13
+ const config = await loadConversationConfiguration();
14
14
  return config.conversations[conversationKey(profile.name, options.conversation)]?.previousResponseId;
15
15
  }
16
16
  export async function updateConversation(options, responseID) {
17
17
  if (!options.conversation)
18
18
  return;
19
19
  const profile = await activeProfile(options.profile);
20
- const config = await loadConfig();
20
+ const config = await loadConversationConfiguration();
21
21
  config.conversations[conversationKey(profile.name, options.conversation)] = {
22
22
  name: options.conversation,
23
23
  profile: profile.name,
24
24
  previousResponseId: responseID,
25
25
  updatedAt: Math.floor(Date.now() / 1000),
26
26
  };
27
- await saveConfig(config);
27
+ await saveConversationConfiguration(config);
28
28
  }
29
29
  export async function listConversations(profileName) {
30
30
  const profile = await activeProfile(profileName);
31
- const config = await loadConfig();
31
+ const config = await loadConversationConfiguration();
32
32
  return Object.values(config.conversations)
33
33
  .filter((conversation) => conversation.profile === profile.name)
34
34
  .sort((a, b) => b.updatedAt - a.updatedAt);
35
35
  }
36
36
  export async function getConversation(name, profileName) {
37
37
  const profile = await activeProfile(profileName);
38
- const config = await loadConfig();
38
+ const config = await loadConversationConfiguration();
39
39
  const conversation = config.conversations[conversationKey(profile.name, name)];
40
40
  if (!conversation)
41
41
  throw new Error(`Conversation not found: ${name}`);
@@ -43,9 +43,9 @@ export async function getConversation(name, profileName) {
43
43
  }
44
44
  export async function deleteConversation(name, profileName) {
45
45
  const profile = await activeProfile(profileName);
46
- const config = await loadConfig();
46
+ const config = await loadConversationConfiguration();
47
47
  delete config.conversations[conversationKey(profile.name, name)];
48
- await saveConfig(config);
48
+ await saveConversationConfiguration(config);
49
49
  }
50
50
  export function conversationSummary(conversation) {
51
51
  const updated = new Date(conversation.updatedAt * 1000).toISOString();
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import React from "react";
7
7
  import { conversationSummary, deleteConversation, getConversation, listConversations, runAgent } from "./agent.js";
8
8
  import { normalizeChatOptions } from "./chat-options.js";
9
9
  import { ChatApp } from "./tui/chat.js";
10
- import { activeProfile, loadConfig, redactSecret } from "./config.js";
10
+ import { activeProfile, loadConfig, loadConversationConfiguration, redactSecret } from "./config.js";
11
11
  import { cliVersion, runtime } from "./runtime/index.js";
12
12
  import { openWorkdir } from "./workdir/index.js";
13
13
  import { deleteProfile, listProfiles, loginWithAPIKey, loginWithBrowser, profileSummary, resolveRuntimeProfile, useProfile, } from "./profile.js";
@@ -64,12 +64,13 @@ program
64
64
  .description("Print local CLI diagnostics")
65
65
  .action(async () => {
66
66
  const config = await loadConfig();
67
+ const conversations = await loadConversationConfiguration();
67
68
  const { profiles } = await listProfiles();
68
69
  console.log(JSON.stringify({
69
70
  version: cliVersion,
70
71
  activeProfile: config.activeProfile,
71
72
  profileCount: profiles.length,
72
- conversationCount: Object.keys(config.conversations).length,
73
+ conversationCount: Object.keys(conversations.conversations).length,
73
74
  configDir: runtime.dirs.config,
74
75
  dataDir: runtime.dirs.data,
75
76
  node: process.version,
@@ -1,5 +1,5 @@
1
- export declare const cliName = "agent-api-cli";
1
+ export declare const cliName = "agent-tui";
2
2
  export declare const cliAuthor = "AgentsWay";
3
- export declare const cliVersion = "0.1.2";
3
+ export declare const cliVersion = "0.2.1";
4
4
  export declare const runtime: import("@agent-api/sdk/local").LocalRuntime;
5
5
  export declare function ensureRuntime(): Promise<import("@agent-api/sdk/local").LocalRuntime>;
@@ -1,12 +1,152 @@
1
1
  import { createLocalRuntime } from "@agent-api/sdk/local";
2
- export const cliName = "agent-api-cli";
2
+ import { cp, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ export const cliName = "agent-tui";
3
5
  export const cliAuthor = "AgentsWay";
4
- export const cliVersion = "0.1.2";
6
+ export const cliVersion = "0.2.1";
7
+ const legacyCliName = "agent-api-cli";
5
8
  export const runtime = createLocalRuntime({
6
9
  appName: cliName,
7
10
  appAuthor: cliAuthor,
8
11
  });
12
+ const legacyRuntime = createLocalRuntime({
13
+ appName: legacyCliName,
14
+ appAuthor: cliAuthor,
15
+ });
16
+ let migrationPromise = null;
9
17
  export async function ensureRuntime() {
18
+ migrationPromise ??= migrateLegacyRuntime();
19
+ await migrationPromise;
10
20
  await runtime.ensure();
21
+ await splitMonolithicConfig();
11
22
  return runtime;
12
23
  }
24
+ async function migrateLegacyRuntime() {
25
+ await migrateLegacyConfigDirectory();
26
+ for (const key of ["data", "cache", "logs"]) {
27
+ await moveDirectoryIfNeeded(legacyRuntime.dirs[key], runtime.dirs[key]);
28
+ }
29
+ }
30
+ async function moveDirectoryIfNeeded(from, to) {
31
+ if (from === to || !(await isDirectory(from)))
32
+ return;
33
+ await mkdir(path.dirname(to), { recursive: true });
34
+ if (await pathExists(to)) {
35
+ await cp(from, to, { recursive: true, errorOnExist: false, force: false });
36
+ await rm(from, { recursive: true, force: true });
37
+ return;
38
+ }
39
+ try {
40
+ await rename(from, to);
41
+ }
42
+ catch (error) {
43
+ if (error?.code !== "EXDEV")
44
+ throw error;
45
+ await cp(from, to, { recursive: true, errorOnExist: false });
46
+ await rm(from, { recursive: true, force: true });
47
+ }
48
+ }
49
+ async function migrateLegacyConfigDirectory() {
50
+ const from = legacyRuntime.dirs.config;
51
+ const to = runtime.dirs.config;
52
+ if (from === to || !(await isDirectory(from)))
53
+ return;
54
+ await mkdir(to, { recursive: true });
55
+ const legacyProfilesPath = path.join(from, "profiles.json");
56
+ const profilesPath = path.join(to, "profiles.json");
57
+ const legacyRaw = await readJSONRecord(legacyProfilesPath);
58
+ if (legacyRaw) {
59
+ const nextRaw = await readJSONRecord(profilesPath) ?? {};
60
+ const legacyProfiles = recordValue(legacyRaw.profiles);
61
+ const nextProfiles = recordValue(nextRaw.profiles);
62
+ const mergedProfiles = { ...legacyProfiles, ...nextProfiles };
63
+ const activeProfile = typeof nextRaw.activeProfile === "string"
64
+ ? nextRaw.activeProfile
65
+ : typeof legacyRaw.activeProfile === "string"
66
+ ? legacyRaw.activeProfile
67
+ : "default";
68
+ await writeJSON(profilesPath, { ...nextRaw, activeProfile, profiles: mergedProfiles });
69
+ const configurationPath = path.join(to, "configuration.json");
70
+ if ("workbench" in legacyRaw && !(await pathExists(configurationPath))) {
71
+ await writeJSON(configurationPath, { workbench: recordValue(legacyRaw.workbench) });
72
+ }
73
+ const conversationsPath = path.join(to, "conversations.json");
74
+ if ("conversations" in legacyRaw && !(await pathExists(conversationsPath))) {
75
+ await writeJSON(conversationsPath, { conversations: recordValue(legacyRaw.conversations) });
76
+ }
77
+ }
78
+ await copyFileIfMissing(path.join(from, "configuration.json"), path.join(to, "configuration.json"));
79
+ await copyFileIfMissing(path.join(from, "conversations.json"), path.join(to, "conversations.json"));
80
+ await rm(from, { recursive: true, force: true });
81
+ }
82
+ async function splitMonolithicConfig() {
83
+ const profilesPath = path.join(runtime.dirs.config, "profiles.json");
84
+ const raw = await readJSONRecord(profilesPath);
85
+ if (!raw)
86
+ return;
87
+ let changed = false;
88
+ if ("workbench" in raw && !(await pathExists(path.join(runtime.dirs.config, "configuration.json")))) {
89
+ await writeJSON(path.join(runtime.dirs.config, "configuration.json"), { workbench: raw.workbench && typeof raw.workbench === "object" ? raw.workbench : {} });
90
+ }
91
+ if ("workbench" in raw) {
92
+ delete raw.workbench;
93
+ changed = true;
94
+ }
95
+ if ("conversations" in raw && !(await pathExists(path.join(runtime.dirs.config, "conversations.json")))) {
96
+ await writeJSON(path.join(runtime.dirs.config, "conversations.json"), {
97
+ conversations: raw.conversations && typeof raw.conversations === "object" ? raw.conversations : {},
98
+ });
99
+ }
100
+ if ("conversations" in raw) {
101
+ delete raw.conversations;
102
+ changed = true;
103
+ }
104
+ if (changed) {
105
+ await writeJSON(profilesPath, raw);
106
+ }
107
+ }
108
+ function recordValue(value) {
109
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
110
+ }
111
+ async function readJSONRecord(file) {
112
+ try {
113
+ const value = JSON.parse(await readFile(file, "utf8"));
114
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
115
+ }
116
+ catch (error) {
117
+ if (error?.code === "ENOENT")
118
+ return null;
119
+ throw error;
120
+ }
121
+ }
122
+ async function writeJSON(file, value) {
123
+ await mkdir(path.dirname(file), { recursive: true });
124
+ await writeFile(file, `${JSON.stringify(value, null, 2)}\n`);
125
+ }
126
+ async function copyFileIfMissing(from, to) {
127
+ if (!(await pathExists(from)) || await pathExists(to))
128
+ return;
129
+ await mkdir(path.dirname(to), { recursive: true });
130
+ await cp(from, to, { errorOnExist: true, force: false });
131
+ }
132
+ async function pathExists(file) {
133
+ try {
134
+ await stat(file);
135
+ return true;
136
+ }
137
+ catch (error) {
138
+ if (error?.code === "ENOENT")
139
+ return false;
140
+ throw error;
141
+ }
142
+ }
143
+ async function isDirectory(file) {
144
+ try {
145
+ return (await stat(file)).isDirectory();
146
+ }
147
+ catch (error) {
148
+ if (error?.code === "ENOENT")
149
+ return false;
150
+ throw error;
151
+ }
152
+ }
@@ -25,6 +25,7 @@ function AuthenticatedChatApp({ options }) {
25
25
  }
26
26
  const authGateController = authGateControllerRef.current;
27
27
  const [currentProfile, setCurrentProfile] = useState(options.profile || "default");
28
+ const [authCursorVisible, setAuthCursorVisible] = useState(true);
28
29
  const [auth, setAuth] = useState(() => authGateController.initialState({
29
30
  apiKey: process.env.AGENT_API_KEY || "",
30
31
  baseURL: process.env.AGENT_API_BASE_URL || defaultBaseURL,
@@ -44,6 +45,16 @@ function AuthenticatedChatApp({ options }) {
44
45
  mounted = false;
45
46
  };
46
47
  }, [authGateController, options.profile]);
48
+ useEffect(() => {
49
+ if (!isAuthInputStatus(auth.status)) {
50
+ setAuthCursorVisible(true);
51
+ return;
52
+ }
53
+ const interval = setInterval(() => {
54
+ setAuthCursorVisible((visible) => !visible);
55
+ }, 500);
56
+ return () => clearInterval(interval);
57
+ }, [auth.status]);
47
58
  useInput((input, key) => {
48
59
  const result = authGateController.handleInput(input, key, auth);
49
60
  if (result.state !== auth)
@@ -87,7 +98,14 @@ function AuthenticatedChatApp({ options }) {
87
98
  setAuth((current) => authGateController.requestSwitchProfile(current, currentProfile, name));
88
99
  }, options: { ...options, profile: currentProfile }, profileName: currentProfile, authController: authController }));
89
100
  }
90
- return _jsx(InkAuthGate, { state: auth });
101
+ return _jsx(InkAuthGate, { cursorVisible: authCursorVisible, state: auth });
102
+ }
103
+ function isAuthInputStatus(status) {
104
+ return status === "api_profile"
105
+ || status === "api_base_url"
106
+ || status === "api_key"
107
+ || status === "browser_profile"
108
+ || status === "browser_base_url";
91
109
  }
92
110
  function WorkbenchApp({ authController, onLogin, onLogout, onDeleteProfile, onSwitchProfile, options, profileName, }) {
93
111
  const app = useApp();
@@ -167,6 +185,16 @@ function WorkbenchApp({ authController, onLogin, onLogout, onDeleteProfile, onSw
167
185
  if (!mounted)
168
186
  return;
169
187
  dispatch({ type: "settings.set", settings });
188
+ if (settings.activity) {
189
+ dispatch({ type: "activity.add", level: "success", text: settings.activity });
190
+ }
191
+ if (settings.notice) {
192
+ dispatch({ type: "message.add", role: "system", text: settings.notice });
193
+ dispatch({ type: "activity.add", level: "warning", text: "Shell isolation setup is not configured" });
194
+ }
195
+ if (settings.warning) {
196
+ dispatch({ type: "activity.add", level: "warning", text: settings.warning });
197
+ }
170
198
  })
171
199
  .catch((error) => {
172
200
  if (!mounted)
@@ -5,6 +5,7 @@ export declare function InkWorkbenchScreen({ renderModel, spinnerFrame, }: {
5
5
  renderModel: WorkbenchRenderModel;
6
6
  spinnerFrame: number;
7
7
  }): React.JSX.Element;
8
- export declare function InkAuthGate({ state }: {
8
+ export declare function InkAuthGate({ cursorVisible, state }: {
9
+ cursorVisible: boolean;
9
10
  state: AuthGateState;
10
11
  }): React.JSX.Element;
@@ -6,11 +6,11 @@ import { activityColor, } from "../workbench.js";
6
6
  export function InkWorkbenchScreen({ renderModel, spinnerFrame, }) {
7
7
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Header, { contextEnabled: renderModel.header.contextEnabled, conversation: renderModel.header.conversation, model: renderModel.header.model, accessMode: renderModel.header.accessMode, pendingLocalLabel: renderModel.header.pendingLocalLabel, preset: renderModel.header.preset, profile: renderModel.header.profile, renderMode: renderModel.header.renderMode, workdir: renderModel.header.workdir }), _jsxs(Box, { marginTop: 1, height: renderModel.viewportHeight, children: [_jsxs(Box, { flexDirection: "column", width: "72%", paddingRight: 1, children: [renderModel.transcript.visibleLines.map((line) => (_jsx(Text, { bold: line.bold, color: line.color, inverse: line.inverse, wrap: "truncate", children: line.text || " " }, line.id))), renderModel.transcript.visibleLines.length === 0 && _jsx(Text, { color: "gray", children: "No transcript lines." })] }), _jsxs(Box, { flexDirection: "column", width: "28%", height: renderModel.activityHeight, borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, wrap: "truncate", children: "Activity" }), renderModel.visibleActivities.map((activity) => (_jsxs(Text, { color: activityColor(activity.level), wrap: "truncate", children: [new Date(activity.timestamp).toLocaleTimeString(), " ", activity.text] }, activity.id)))] })] }), _jsxs(Box, { borderStyle: "single", borderColor: renderModel.input.busy ? "yellow" : "green", paddingX: 1, children: [renderModel.input.fullAccess && (_jsx(Text, { color: "red", bold: true, inverse: true, children: "FULL ACCESS" })), renderModel.input.fullAccess && _jsx(Text, { children: " " }), _jsxs(Text, { color: renderModel.input.busy ? "yellow" : "green", children: [renderModel.input.label, " "] }), renderModel.input.busy ? (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: "yellow", children: busySpinner(spinnerFrame) }), " ", renderModel.input.waitingText] })) : (_jsxs(Text, { wrap: "truncate", children: [renderModel.input.draft, _jsx(Cursor, { visible: true })] }))] }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { color: "gray", wrap: "truncate", children: renderModel.footerText }) })] }));
8
8
  }
9
- export function InkAuthGate({ state }) {
10
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Agent API Workbench" }), _jsx(Text, { color: "gray", children: "Authentication required before starting the conversation UI." })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: state.error ? "red" : "gray", children: state.error || state.message }), state.status === "checking" && _jsx(Text, { color: "yellow", children: "Checking..." }), state.status === "select" && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [authMethods.map((method, index) => (_jsxs(Text, { color: index === state.selectedMethod ? "green" : "gray", children: [index === state.selectedMethod ? "›" : " ", " ", method.label, " - ", method.description] }, method.method))), _jsx(Text, { color: "gray", children: "Use \u2191/\u2193 and Enter." })] })), state.status === "api_profile" && _jsx(AuthPrompt, { label: "Profile", value: state.profile }), state.status === "api_base_url" && _jsx(AuthPrompt, { label: "Base URL", value: state.baseURL }), state.status === "api_key" && _jsx(AuthPrompt, { label: "API key", value: state.apiKey ? "•".repeat(Math.min(state.apiKey.length, 32)) : "" }), state.status === "browser_profile" && _jsx(AuthPrompt, { label: "Profile", value: state.profile }), state.status === "browser_base_url" && _jsx(AuthPrompt, { label: "Base URL", value: state.baseURL }), state.status === "browser_waiting" && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [state.browserURL && _jsxs(Text, { children: ["URL: ", state.browserURL] }), state.browserCode && _jsxs(Text, { children: ["Code: ", state.browserCode] }), _jsx(Text, { color: "yellow", children: "Waiting for browser approval..." })] }))] })] }));
9
+ export function InkAuthGate({ cursorVisible, state }) {
10
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Agent API Workbench" }), _jsx(Text, { color: "gray", children: "Authentication required before starting the conversation UI." })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: state.error ? "red" : "gray", children: state.error || state.message }), state.status === "checking" && _jsx(Text, { color: "yellow", children: "Checking..." }), state.status === "select" && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [authMethods.map((method, index) => (_jsxs(Text, { color: index === state.selectedMethod ? "green" : "gray", children: [index === state.selectedMethod ? "›" : " ", " ", method.label, " - ", method.description] }, method.method))), _jsx(Text, { color: "gray", children: "Use \u2191/\u2193 and Enter." })] })), state.status === "api_profile" && _jsx(AuthPrompt, { cursorVisible: cursorVisible, label: "Profile", value: state.profile }), state.status === "api_base_url" && _jsx(AuthPrompt, { cursorVisible: cursorVisible, label: "Base URL", value: state.baseURL }), state.status === "api_key" && _jsx(AuthPrompt, { cursorVisible: cursorVisible, label: "API key", value: state.apiKey ? "•".repeat(Math.min(state.apiKey.length, 32)) : "" }), state.status === "browser_profile" && _jsx(AuthPrompt, { cursorVisible: cursorVisible, label: "Profile", value: state.profile }), state.status === "browser_base_url" && _jsx(AuthPrompt, { cursorVisible: cursorVisible, label: "Base URL", value: state.baseURL }), state.status === "browser_waiting" && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [state.browserURL && _jsxs(Text, { children: ["URL: ", state.browserURL] }), state.browserCode && _jsxs(Text, { children: ["Code: ", state.browserCode] }), _jsx(Text, { color: "yellow", children: "Waiting for browser approval..." })] }))] })] }));
11
11
  }
12
- function AuthPrompt({ label, value }) {
13
- return (_jsxs(Box, { borderStyle: "single", borderColor: "green", paddingX: 1, marginTop: 1, children: [_jsxs(Text, { color: "green", children: [label, ": "] }), _jsx(Text, { children: value })] }));
12
+ function AuthPrompt({ cursorVisible, label, value }) {
13
+ return (_jsxs(Box, { borderStyle: "single", borderColor: "green", paddingX: 1, marginTop: 1, children: [_jsxs(Text, { color: "green", children: [label, ": "] }), _jsxs(Text, { children: [value, _jsx(Cursor, { visible: cursorVisible })] })] }));
14
14
  }
15
15
  function Header({ contextEnabled, conversation, accessMode, model, pendingLocalLabel, preset, profile, renderMode, workdir, }) {
16
16
  return (_jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Agent API Workbench" }), _jsxs(Text, { color: "gray", wrap: "truncate", children: ["profile=", profile, " conversation=", conversation, " preset=", preset, " model=", model] }), _jsxs(Text, { color: "gray", wrap: "truncate", children: ["workdir=", workdir, " access=", accessMode, " local_tools=", contextEnabled ? "on" : "off", " render=", renderMode, " pending=", pendingLocalLabel] })] }));
@@ -1,4 +1,5 @@
1
1
  import type { LocalToolApprovalRequest, WorkdirAccessMode } from "../agent.js";
2
+ import type { ShellIsolationPreferences } from "../workbench/shell-isolation.js";
2
3
  export type WorkbenchRole = "user" | "assistant" | "system";
3
4
  export interface WorkbenchMessage {
4
5
  id: string;
@@ -38,6 +39,7 @@ export interface WorkbenchState {
38
39
  runModel?: string;
39
40
  renderMode: RenderMode;
40
41
  defaultPreset?: string | null;
42
+ shellIsolation?: ShellIsolationPreferences;
41
43
  }
42
44
  export interface InputHistory {
43
45
  record(value: string): void;
@@ -88,7 +90,7 @@ export type WorkbenchAction = {
88
90
  name: string;
89
91
  } | {
90
92
  type: "settings.set";
91
- settings: Partial<Pick<WorkbenchState, "runPreset" | "runModel" | "renderMode" | "defaultPreset">>;
93
+ settings: Partial<Pick<WorkbenchState, "runPreset" | "runModel" | "renderMode" | "defaultPreset" | "shellIsolation">>;
92
94
  };
93
95
  export type WorkbenchCommand = {
94
96
  kind: "invalid";
@@ -114,7 +116,7 @@ export type WorkbenchCommand = {
114
116
  kind: "auth_status";
115
117
  } | {
116
118
  kind: "config";
117
- field?: "preset";
119
+ field?: "preset" | "isolation" | "isolator";
118
120
  value?: string;
119
121
  } | {
120
122
  kind: "render";
@@ -171,6 +173,7 @@ export declare function createInitialWorkbenchState(options: {
171
173
  model?: string;
172
174
  renderMode?: RenderMode;
173
175
  defaultPreset?: string | null;
176
+ shellIsolation?: ShellIsolationPreferences;
174
177
  }): WorkbenchState;
175
178
  export declare function createInputHistory(limit?: number): InputHistory;
176
179
  export declare function workbenchReducer(state: WorkbenchState, action: WorkbenchAction): WorkbenchState;
@@ -18,6 +18,7 @@ export function createInitialWorkbenchState(options) {
18
18
  runModel: options.model,
19
19
  renderMode: options.renderMode ?? "markdown",
20
20
  defaultPreset: options.defaultPreset,
21
+ shellIsolation: options.shellIsolation,
21
22
  };
22
23
  }
23
24
  export function createInputHistory(limit = 100) {
@@ -188,7 +189,7 @@ export function parseWorkbenchCommand(input) {
188
189
  const [field, ...valueParts] = rest;
189
190
  if (!field)
190
191
  return { kind: "config" };
191
- if (field === "preset") {
192
+ if (field === "preset" || field === "isolation" || field === "isolator") {
192
193
  return { kind: "config", field, value: valueParts.join(" ").trim() || undefined };
193
194
  }
194
195
  return { kind: "invalid", command: `${name} ${field}` };
@@ -290,6 +291,8 @@ export function helpText() {
290
291
  "/transcript show a plain-text transcript preview",
291
292
  "/export [file] save the plain-text transcript to a file",
292
293
  "/config preset save default preset; use none/off for no preset, reset for built-in",
294
+ "/config isolation save shell isolation mode: none, auto, or required",
295
+ "/config isolator save agent-isolator path; use none/off to clear",
293
296
  "/preset [name] show or set preset; use none/off to clear",
294
297
  "/model [name] show or set explicit model; use auto/none/off to clear",
295
298
  "/access [mode] show or set local tool access: off, approval, or full",