@agent-api/cli 0.0.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.
package/dist/config.js ADDED
@@ -0,0 +1,111 @@
1
+ import { z } from "zod";
2
+ import { runtime } from "./runtime/index.js";
3
+ export const defaultBaseURL = "https://api.agentsway.dev";
4
+ export const configFile = "profiles.json";
5
+ const authProfileSchema = z.discriminatedUnion("type", [
6
+ z.object({
7
+ type: z.literal("api_key"),
8
+ apiKey: z.string(),
9
+ }),
10
+ z.object({
11
+ type: z.literal("browser"),
12
+ accessToken: z.string(),
13
+ refreshToken: z.string(),
14
+ accessTokenExpiresAt: z.number(),
15
+ refreshTokenExpiresAt: z.number().optional(),
16
+ }),
17
+ ]);
18
+ const profileSchema = z.object({
19
+ name: z.string(),
20
+ baseURL: z.string(),
21
+ auth: authProfileSchema,
22
+ createdAt: z.number(),
23
+ updatedAt: z.number(),
24
+ });
25
+ const conversationSchema = z.object({
26
+ name: z.string(),
27
+ profile: z.string(),
28
+ previousResponseId: z.string().optional(),
29
+ updatedAt: z.number(),
30
+ });
31
+ const workbenchPreferencesSchema = z.object({
32
+ defaultPreset: z.string().nullable().optional(),
33
+ }).default({});
34
+ const cliConfigSchema = z.object({
35
+ activeProfile: z.string().default("default"),
36
+ profiles: z.record(z.string(), profileSchema).default({}),
37
+ conversations: z.record(z.string(), conversationSchema).default({}),
38
+ workbench: workbenchPreferencesSchema,
39
+ });
40
+ export async function loadConfig() {
41
+ await runtime.ensure();
42
+ const loaded = await runtime.config.read(configFile, emptyConfig());
43
+ const parsed = cliConfigSchema.safeParse(loaded);
44
+ if (!parsed.success) {
45
+ throw new Error(`Invalid CLI config: ${parsed.error.issues.map((issue) => issue.message).join("; ")}`);
46
+ }
47
+ return parsed.data;
48
+ }
49
+ export async function saveConfig(config) {
50
+ await runtime.ensure();
51
+ await runtime.config.write(configFile, config);
52
+ }
53
+ export async function upsertProfile(profile) {
54
+ const config = await loadConfig();
55
+ const now = Math.floor(Date.now() / 1000);
56
+ const existing = config.profiles[profile.name];
57
+ const next = {
58
+ ...profile,
59
+ createdAt: existing?.createdAt ?? now,
60
+ updatedAt: now,
61
+ };
62
+ config.profiles[profile.name] = next;
63
+ config.activeProfile = profile.name;
64
+ await saveConfig(config);
65
+ return next;
66
+ }
67
+ export async function activeProfile(profileName) {
68
+ const config = await loadConfig();
69
+ const name = profileName || config.activeProfile || "default";
70
+ const profile = config.profiles[name];
71
+ if (!profile) {
72
+ throw new Error(`Profile not found: ${name}. Run agent-api login first.`);
73
+ }
74
+ return profile;
75
+ }
76
+ export function emptyConfig() {
77
+ return { activeProfile: "default", profiles: {}, conversations: {}, workbench: {} };
78
+ }
79
+ export async function loadWorkbenchPreferences() {
80
+ const config = await loadConfig();
81
+ return config.workbench;
82
+ }
83
+ export async function updateWorkbenchPreferences(patch) {
84
+ const config = await loadConfig();
85
+ const next = { ...config.workbench };
86
+ if ("defaultPreset" in patch) {
87
+ if (patch.defaultPreset === undefined) {
88
+ delete next.defaultPreset;
89
+ }
90
+ else if (patch.defaultPreset === null) {
91
+ next.defaultPreset = null;
92
+ }
93
+ else {
94
+ const value = patch.defaultPreset.trim();
95
+ if (value) {
96
+ next.defaultPreset = value;
97
+ }
98
+ else {
99
+ delete next.defaultPreset;
100
+ }
101
+ }
102
+ }
103
+ config.workbench = next;
104
+ await saveConfig(config);
105
+ return next;
106
+ }
107
+ export function redactSecret(value) {
108
+ if (value.length <= 10)
109
+ return "***";
110
+ return `${value.slice(0, 6)}…${value.slice(-4)}`;
111
+ }
@@ -0,0 +1,17 @@
1
+ import type { ConversationState } from "../config.js";
2
+ export declare function conversationKey(profile: string, name: string): string;
3
+ export declare function resolvePreviousResponseID(options: {
4
+ profile?: string;
5
+ conversation?: string;
6
+ continueConversation?: boolean;
7
+ restartConversation?: boolean;
8
+ previousResponseId?: string;
9
+ }): Promise<string | undefined>;
10
+ export declare function updateConversation(options: {
11
+ profile?: string;
12
+ conversation?: string;
13
+ }, responseID: string): Promise<void>;
14
+ export declare function listConversations(profileName?: string): Promise<ConversationState[]>;
15
+ export declare function getConversation(name: string, profileName?: string): Promise<ConversationState>;
16
+ export declare function deleteConversation(name: string, profileName?: string): Promise<void>;
17
+ export declare function conversationSummary(conversation: ConversationState): string;
@@ -0,0 +1,54 @@
1
+ import { activeProfile, loadConfig, saveConfig } from "../config.js";
2
+ export function conversationKey(profile, name) {
3
+ return `${profile}:${name}`;
4
+ }
5
+ export async function resolvePreviousResponseID(options) {
6
+ if (options.previousResponseId)
7
+ return options.previousResponseId;
8
+ if (options.restartConversation || !options.conversation)
9
+ return undefined;
10
+ if (!options.continueConversation)
11
+ return undefined;
12
+ const profile = await activeProfile(options.profile);
13
+ const config = await loadConfig();
14
+ return config.conversations[conversationKey(profile.name, options.conversation)]?.previousResponseId;
15
+ }
16
+ export async function updateConversation(options, responseID) {
17
+ if (!options.conversation)
18
+ return;
19
+ const profile = await activeProfile(options.profile);
20
+ const config = await loadConfig();
21
+ config.conversations[conversationKey(profile.name, options.conversation)] = {
22
+ name: options.conversation,
23
+ profile: profile.name,
24
+ previousResponseId: responseID,
25
+ updatedAt: Math.floor(Date.now() / 1000),
26
+ };
27
+ await saveConfig(config);
28
+ }
29
+ export async function listConversations(profileName) {
30
+ const profile = await activeProfile(profileName);
31
+ const config = await loadConfig();
32
+ return Object.values(config.conversations)
33
+ .filter((conversation) => conversation.profile === profile.name)
34
+ .sort((a, b) => b.updatedAt - a.updatedAt);
35
+ }
36
+ export async function getConversation(name, profileName) {
37
+ const profile = await activeProfile(profileName);
38
+ const config = await loadConfig();
39
+ const conversation = config.conversations[conversationKey(profile.name, name)];
40
+ if (!conversation)
41
+ throw new Error(`Conversation not found: ${name}`);
42
+ return conversation;
43
+ }
44
+ export async function deleteConversation(name, profileName) {
45
+ const profile = await activeProfile(profileName);
46
+ const config = await loadConfig();
47
+ delete config.conversations[conversationKey(profile.name, name)];
48
+ await saveConfig(config);
49
+ }
50
+ export function conversationSummary(conversation) {
51
+ const updated = new Date(conversation.updatedAt * 1000).toISOString();
52
+ const response = conversation.previousResponseId ? ` response=${conversation.previousResponseId}` : "";
53
+ return `${conversation.name}\t${conversation.profile}\t${updated}${response}`;
54
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,308 @@
1
+ #!/usr/bin/env node
2
+ import { Command, Option } from "commander";
3
+ import { render } from "ink";
4
+ import React from "react";
5
+ import { conversationSummary, deleteConversation, getConversation, listConversations, runAgent } from "./agent.js";
6
+ import { normalizeChatOptions } from "./chat-options.js";
7
+ import { ChatApp } from "./tui/chat.js";
8
+ import { activeProfile, loadConfig, redactSecret } from "./config.js";
9
+ import { cliVersion, runtime } from "./runtime/index.js";
10
+ import { openWorkdir } from "./workdir/index.js";
11
+ import { deleteProfile, listProfiles, loginWithAPIKey, loginWithBrowser, profileSummary, resolveRuntimeProfile, useProfile, } from "./profile.js";
12
+ const program = new Command();
13
+ program
14
+ .name("agent-api")
15
+ .alias("agentsway")
16
+ .alias("agent-tui")
17
+ .description("First-class command line interface for Agent API")
18
+ .version(cliVersion)
19
+ .showHelpAfterError()
20
+ .showSuggestionAfterError();
21
+ program.action(async () => {
22
+ if (!process.stdin.isTTY) {
23
+ program.help();
24
+ return;
25
+ }
26
+ const options = normalizeChatOptions([], {});
27
+ const app = render(React.createElement(ChatApp, { options }));
28
+ await app.waitUntilExit();
29
+ });
30
+ program
31
+ .command("auth")
32
+ .description("Manage authentication")
33
+ .addCommand(authLoginCommand())
34
+ .addCommand(authWhoamiCommand())
35
+ .addCommand(authLogoutCommand());
36
+ program
37
+ .command("profiles")
38
+ .alias("profile")
39
+ .description("Manage local auth profiles")
40
+ .addCommand(profilesListCommand())
41
+ .addCommand(profilesUseCommand())
42
+ .addCommand(profilesShowCommand())
43
+ .addCommand(profilesDeleteCommand());
44
+ program
45
+ .command("agent")
46
+ .description("Chat with and manage remote agent conversations")
47
+ .addCommand(agentChatCommand())
48
+ .addCommand(agentListCommand())
49
+ .addCommand(agentShowCommand())
50
+ .addCommand(agentDeleteCommand());
51
+ program
52
+ .command("workdir")
53
+ .alias("ws")
54
+ .description("Inspect and package local workdir context")
55
+ .addCommand(workdirStatusCommand())
56
+ .addCommand(workdirSummaryCommand())
57
+ .addCommand(workdirContextCommand());
58
+ program
59
+ .command("doctor")
60
+ .description("Print local CLI diagnostics")
61
+ .action(async () => {
62
+ const config = await loadConfig();
63
+ const { profiles } = await listProfiles();
64
+ console.log(JSON.stringify({
65
+ version: cliVersion,
66
+ activeProfile: config.activeProfile,
67
+ profileCount: profiles.length,
68
+ conversationCount: Object.keys(config.conversations).length,
69
+ configDir: runtime.dirs.config,
70
+ dataDir: runtime.dirs.data,
71
+ node: process.version,
72
+ platform: process.platform,
73
+ }, null, 2));
74
+ });
75
+ program.exitOverride();
76
+ program.parseAsync(process.argv).catch((error) => {
77
+ if (error?.code === "commander.helpDisplayed" || error?.code === "commander.version")
78
+ return;
79
+ console.error(error instanceof Error ? error.message : String(error));
80
+ process.exitCode = 1;
81
+ });
82
+ function authLoginCommand() {
83
+ return new Command("login")
84
+ .description("Sign in with browser auth or save an API key profile")
85
+ .option("-p, --profile <name>", "profile name", "default")
86
+ .option("--base-url <url>", "Agent API base URL")
87
+ .option("--api-key <key>", "API key; defaults to AGENT_API_KEY")
88
+ .option("--no-browser", "print browser URL without opening it")
89
+ .option("--client-name <name>", "device auth client name", "Agent API CLI")
90
+ .action(async (options) => {
91
+ const apiKey = options.apiKey || process.env.AGENT_API_KEY;
92
+ if (apiKey) {
93
+ const saved = await loginWithAPIKey({ profile: options.profile, baseURL: options.baseUrl, apiKey });
94
+ console.log(`Saved API key profile "${saved.name}" (${saved.baseURL}).`);
95
+ return;
96
+ }
97
+ await loginWithBrowser({
98
+ profile: options.profile,
99
+ baseURL: options.baseUrl,
100
+ openBrowser: options.browser,
101
+ clientName: options.clientName,
102
+ });
103
+ });
104
+ }
105
+ function authWhoamiCommand() {
106
+ return new Command("whoami")
107
+ .alias("status")
108
+ .description("Show the authenticated account for a profile")
109
+ .option("-p, --profile <name>", "profile name")
110
+ .action(async (options) => {
111
+ const runtime = await resolveRuntimeProfile(options.profile);
112
+ const response = await fetch(`${runtime.profile.baseURL}/v1/me`, {
113
+ headers: { Authorization: `Bearer ${runtime.token}` },
114
+ });
115
+ const payload = await response.json().catch(() => ({}));
116
+ if (!response.ok)
117
+ throw new Error(payload?.error?.message || `whoami failed with ${response.status}`);
118
+ console.log(JSON.stringify({ profile: runtime.profile.name, ...payload }, null, 2));
119
+ });
120
+ }
121
+ function authLogoutCommand() {
122
+ return new Command("logout")
123
+ .description("Delete a local auth profile")
124
+ .option("-p, --profile <name>", "profile name")
125
+ .action(async (options) => {
126
+ const profile = options.profile || (await loadConfig()).activeProfile;
127
+ await deleteProfile(profile);
128
+ console.log(`Deleted profile "${profile}".`);
129
+ });
130
+ }
131
+ function profilesListCommand() {
132
+ return new Command("list")
133
+ .description("List local auth profiles")
134
+ .action(async () => {
135
+ const { active, profiles } = await listProfiles();
136
+ if (profiles.length === 0) {
137
+ console.log("No profiles configured. Run agent-api auth login.");
138
+ return;
139
+ }
140
+ for (const profile of profiles) {
141
+ console.log(profileSummary(profile, profile.name === active));
142
+ }
143
+ });
144
+ }
145
+ function profilesUseCommand() {
146
+ return new Command("use")
147
+ .description("Set the active profile")
148
+ .argument("<name>", "profile name")
149
+ .action(async (name) => {
150
+ await useProfile(name);
151
+ console.log(`Active profile: ${name}`);
152
+ });
153
+ }
154
+ function profilesShowCommand() {
155
+ return new Command("show")
156
+ .description("Show a profile without secrets")
157
+ .argument("[name]", "profile name")
158
+ .action(async (name) => {
159
+ const profile = await activeProfile(name);
160
+ console.log(JSON.stringify({
161
+ name: profile.name,
162
+ baseURL: profile.baseURL,
163
+ auth: profile.auth.type === "api_key"
164
+ ? { type: "api_key", apiKey: redactSecret(profile.auth.apiKey) }
165
+ : {
166
+ type: "browser",
167
+ accessToken: redactSecret(profile.auth.accessToken),
168
+ accessTokenExpiresAt: profile.auth.accessTokenExpiresAt,
169
+ refreshTokenExpiresAt: profile.auth.refreshTokenExpiresAt,
170
+ },
171
+ createdAt: profile.createdAt,
172
+ updatedAt: profile.updatedAt,
173
+ }, null, 2));
174
+ });
175
+ }
176
+ function profilesDeleteCommand() {
177
+ return new Command("delete")
178
+ .alias("rm")
179
+ .description("Delete a local auth profile")
180
+ .argument("<name>", "profile name")
181
+ .action(async (name) => {
182
+ await deleteProfile(name);
183
+ console.log(`Deleted profile "${name}".`);
184
+ });
185
+ }
186
+ function agentChatCommand() {
187
+ return new Command("chat")
188
+ .description("Start an interactive chat, or send one message when prompt text is provided")
189
+ .argument("[prompt...]", "prompt text")
190
+ .option("-p, --profile <name>", "profile name")
191
+ .option("-c, --conversation <name>", "conversation name", "default")
192
+ .option("--preset <name>", "agent preset")
193
+ .option("--model <name>", "explicit model")
194
+ .option("--file <path>", "read prompt text from file")
195
+ .option("--stdin", "read prompt text from stdin")
196
+ .option("--workdir <path>", "attach local workdir context")
197
+ .option("--local-context", "attach current directory context")
198
+ .option("--context-query <text>", "include local search matches in context")
199
+ .option("--max-context-files <n>", "local context file limit")
200
+ .option("--max-context-bytes <n>", "local context byte limit")
201
+ .option("--access <mode>", "local tool access mode: off, approval, or full")
202
+ .option("--restart", "start the conversation from a fresh response")
203
+ .addOption(new Option("--no-stream", "wait for final response instead of streaming"))
204
+ .action(async (prompt, options) => {
205
+ const promptParts = prompt ?? [];
206
+ const hasOneShotInput = promptParts.length > 0 || options.file || options.stdin;
207
+ const normalized = normalizeChatOptions(promptParts, options);
208
+ const shouldUseWorkbench = process.stdin.isTTY && (!hasOneShotInput ||
209
+ Boolean(normalized.workdir && normalized.accessMode === "approval" && promptParts.length > 0 && !options.file && !options.stdin));
210
+ if (shouldUseWorkbench) {
211
+ const app = render(React.createElement(ChatApp, { options: normalized }));
212
+ await app.waitUntilExit();
213
+ return;
214
+ }
215
+ await runAgent(normalized);
216
+ });
217
+ }
218
+ function agentListCommand() {
219
+ return new Command("list")
220
+ .alias("conversations")
221
+ .description("List local conversation handles")
222
+ .option("-p, --profile <name>", "profile name")
223
+ .action(async (options) => {
224
+ const conversations = await listConversations(options.profile);
225
+ if (conversations.length === 0) {
226
+ console.log("No agent conversations yet.");
227
+ return;
228
+ }
229
+ for (const conversation of conversations)
230
+ console.log(conversationSummary(conversation));
231
+ });
232
+ }
233
+ function agentShowCommand() {
234
+ return new Command("show")
235
+ .description("Show a local conversation handle")
236
+ .argument("[name]", "conversation name", "default")
237
+ .option("-p, --profile <name>", "profile name")
238
+ .action(async (name, options) => {
239
+ console.log(JSON.stringify(await getConversation(name, options.profile), null, 2));
240
+ });
241
+ }
242
+ function agentDeleteCommand() {
243
+ return new Command("delete")
244
+ .alias("rm")
245
+ .description("Delete a local conversation handle")
246
+ .argument("<name>", "conversation name")
247
+ .option("-p, --profile <name>", "profile name")
248
+ .action(async (name, options) => {
249
+ await deleteConversation(name, options.profile);
250
+ console.log(`Deleted conversation "${name}".`);
251
+ });
252
+ }
253
+ function workdirStatusCommand() {
254
+ return new Command("status")
255
+ .description("Show local workdir status")
256
+ .option("--path <path>", "workdir path", process.cwd())
257
+ .action(async (options) => {
258
+ const workdir = await openWorkdir({ path: options.path });
259
+ const [summary, snapshot] = await Promise.all([
260
+ workdir.summarize(),
261
+ workdir.snapshot(),
262
+ ]);
263
+ console.log(JSON.stringify({
264
+ root: workdir.root,
265
+ name: workdir.name,
266
+ fileCount: summary.file_count,
267
+ totalBytes: summary.total_bytes,
268
+ snapshotFiles: snapshot.files.length,
269
+ scanTruncated: summary.scan_truncated,
270
+ }, null, 2));
271
+ });
272
+ }
273
+ function workdirSummaryCommand() {
274
+ return new Command("summary")
275
+ .description("Summarize local workdir files")
276
+ .option("--path <path>", "workdir path", process.cwd())
277
+ .action(async (options) => {
278
+ const workdir = await openWorkdir({ path: options.path });
279
+ console.log(JSON.stringify(await workdir.summarize(), null, 2));
280
+ });
281
+ }
282
+ function workdirContextCommand() {
283
+ return new Command("context")
284
+ .description("Build the local context package that can be sent to the agent")
285
+ .option("--path <path>", "workdir path", process.cwd())
286
+ .option("--query <text>", "include local search matches")
287
+ .option("--max-files <n>", "maximum files to include")
288
+ .option("--max-bytes <n>", "maximum bytes to include")
289
+ .option("--no-content", "omit file contents")
290
+ .action(async (options) => {
291
+ const workdir = await openWorkdir({ path: options.path });
292
+ const context = await workdir.packageContext({
293
+ query: options.query,
294
+ maxFiles: optionalNumber(options.maxFiles, "--max-files"),
295
+ maxBytes: optionalNumber(options.maxBytes, "--max-bytes"),
296
+ includeContent: options.content !== false,
297
+ });
298
+ console.log(JSON.stringify(context, null, 2));
299
+ });
300
+ }
301
+ function optionalNumber(value, label) {
302
+ if (value == null || value === "")
303
+ return undefined;
304
+ const parsed = Number(value);
305
+ if (!Number.isFinite(parsed))
306
+ throw new Error(`${label} must be a number`);
307
+ return parsed;
308
+ }
@@ -0,0 +1,57 @@
1
+ import { AgentAPI, type ApprovedDeviceAuth } from "@agent-api/sdk";
2
+ import { Profile } from "./config.js";
3
+ export interface RuntimeProfile {
4
+ profile: Profile;
5
+ token: string;
6
+ client: AgentAPI;
7
+ }
8
+ export interface AuthStatus {
9
+ profile: string;
10
+ baseURL: string;
11
+ authType: Profile["auth"]["type"];
12
+ me?: unknown;
13
+ }
14
+ export declare class AuthSessionExpiredError extends Error {
15
+ readonly profile: string;
16
+ readonly baseURL: string;
17
+ constructor(profile: Profile, message?: string);
18
+ }
19
+ export declare function loginWithAPIKey(options: {
20
+ profile: string;
21
+ baseURL?: string;
22
+ apiKey: string;
23
+ }): Promise<Profile>;
24
+ export declare function loginWithBrowser(options: {
25
+ profile: string;
26
+ baseURL?: string;
27
+ clientName?: string;
28
+ openBrowser?: boolean;
29
+ }): Promise<Profile>;
30
+ export declare function startBrowserAuthChallenge(options: {
31
+ baseURL?: string;
32
+ clientName?: string;
33
+ }): Promise<import("@agent-api/sdk").DeviceAuthStart>;
34
+ export declare function waitForBrowserAuthChallenge(options: {
35
+ baseURL?: string;
36
+ challenge: Awaited<ReturnType<typeof startBrowserAuthChallenge>>;
37
+ on_poll?: Parameters<AgentAPI["auth"]["waitForDeviceAuth"]>[0]["on_poll"];
38
+ }): Promise<ApprovedDeviceAuth>;
39
+ export declare function saveBrowserProfile(name: string, baseURL: string, session: ApprovedDeviceAuth): Promise<Profile>;
40
+ export declare function resolveRuntimeProfile(profileName?: string): Promise<RuntimeProfile>;
41
+ export declare function getAuthStatus(profileName?: string): Promise<AuthStatus>;
42
+ export declare function refreshActiveProfileIfNeeded(profileName?: string, refreshWindowMs?: number): Promise<{
43
+ profile: Profile;
44
+ refreshed: boolean;
45
+ }>;
46
+ export declare function refreshIfNeeded(profile: Profile, refreshWindowMs?: number): Promise<Profile>;
47
+ export declare function browserAccessTokenExpiresWithin(profile: Profile, refreshWindowMs: number, now?: number): boolean;
48
+ export declare function refreshBrowserSession(profile: Profile): Promise<Profile>;
49
+ export declare function listProfiles(): Promise<{
50
+ active: string;
51
+ profiles: Profile[];
52
+ }>;
53
+ export declare function useProfile(name: string): Promise<void>;
54
+ export declare function deleteProfile(name: string): Promise<void>;
55
+ export declare function profileSummary(profile: Profile, active?: boolean): string;
56
+ export declare function formatDeviceUserCode(code: string): string;
57
+ export declare function openBrowserURL(url: string): Promise<void>;