@dex-ai/coding-agent-sdk 0.1.21
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 +28 -0
- package/src/create.ts +76 -0
- package/src/extensions/approval.ts +164 -0
- package/src/extensions/config.ts +86 -0
- package/src/extensions/env.ts +281 -0
- package/src/extensions/session.ts +347 -0
- package/src/extensions/settings.ts +416 -0
- package/src/extensions/system-prompt.ts +208 -0
- package/src/extensions/workspace.ts +236 -0
- package/src/index.ts +45 -0
- package/src/types.ts +148 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace Extension — detects git repos, project type, manages .dex/ directory.
|
|
3
|
+
*
|
|
4
|
+
* Two modes:
|
|
5
|
+
* - Single-repo: cwd is inside a git repository → standard behavior.
|
|
6
|
+
* - Multi-repo: cwd is NOT a git repo but contains child directories that are
|
|
7
|
+
* git repos → reports all child repositories for coordinated changes.
|
|
8
|
+
*
|
|
9
|
+
* The workspace layout is injected directly into the system prompt at session
|
|
10
|
+
* start (via session-start event) — no skill/tool fetch required.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Extension, AgentContext, Content } from "@dex-ai/sdk";
|
|
14
|
+
import { existsSync, mkdirSync, readdirSync, statSync } from "node:fs";
|
|
15
|
+
import { resolve, join, basename } from "node:path";
|
|
16
|
+
import { execSync } from "node:child_process";
|
|
17
|
+
import type { Workspace, Repository, ProjectType } from "../types";
|
|
18
|
+
|
|
19
|
+
export interface WorkspaceExtensionOptions {
|
|
20
|
+
cwd?: string;
|
|
21
|
+
/** Maximum depth to scan for git repositories (relative to cwd). Default: 2. */
|
|
22
|
+
maxDepth?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isGitRepo(dir: string): boolean {
|
|
26
|
+
try {
|
|
27
|
+
execSync("git rev-parse --is-inside-work-tree", {
|
|
28
|
+
cwd: dir,
|
|
29
|
+
encoding: "utf-8",
|
|
30
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
31
|
+
});
|
|
32
|
+
return true;
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function detectGitRoot(cwd: string): string | null {
|
|
39
|
+
try {
|
|
40
|
+
return execSync("git rev-parse --show-toplevel", {
|
|
41
|
+
cwd,
|
|
42
|
+
encoding: "utf-8",
|
|
43
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
44
|
+
}).trim();
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function detectBranch(cwd: string): string | undefined {
|
|
51
|
+
try {
|
|
52
|
+
return (
|
|
53
|
+
execSync("git branch --show-current", {
|
|
54
|
+
cwd,
|
|
55
|
+
encoding: "utf-8",
|
|
56
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
57
|
+
}).trim() || undefined
|
|
58
|
+
);
|
|
59
|
+
} catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function detectProjectType(root: string): ProjectType {
|
|
65
|
+
if (existsSync(join(root, "package.json"))) return "node";
|
|
66
|
+
if (
|
|
67
|
+
existsSync(join(root, "pyproject.toml")) ||
|
|
68
|
+
existsSync(join(root, "setup.py"))
|
|
69
|
+
)
|
|
70
|
+
return "python";
|
|
71
|
+
if (existsSync(join(root, "Cargo.toml"))) return "rust";
|
|
72
|
+
if (existsSync(join(root, "go.mod"))) return "go";
|
|
73
|
+
if (
|
|
74
|
+
existsSync(join(root, "pom.xml")) ||
|
|
75
|
+
existsSync(join(root, "build.gradle"))
|
|
76
|
+
)
|
|
77
|
+
return "java";
|
|
78
|
+
return "unknown";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Recursively scan directories for git repositories up to `maxDepth` levels.
|
|
83
|
+
* Stops descending into a directory once it's identified as a git repo.
|
|
84
|
+
*/
|
|
85
|
+
function discoverRepos(cwd: string, maxDepth: number): Repository[] {
|
|
86
|
+
const repos: Repository[] = [];
|
|
87
|
+
|
|
88
|
+
function scan(dir: string, depth: number): void {
|
|
89
|
+
if (depth > maxDepth) return;
|
|
90
|
+
|
|
91
|
+
let entries: string[];
|
|
92
|
+
try {
|
|
93
|
+
entries = readdirSync(dir);
|
|
94
|
+
} catch {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const entry of entries) {
|
|
99
|
+
// Skip hidden dirs and node_modules
|
|
100
|
+
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
101
|
+
|
|
102
|
+
const childPath = join(dir, entry);
|
|
103
|
+
try {
|
|
104
|
+
if (!statSync(childPath).isDirectory()) continue;
|
|
105
|
+
} catch {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (isGitRepo(childPath)) {
|
|
110
|
+
// Found a git repo — add it and don't descend further
|
|
111
|
+
repos.push({
|
|
112
|
+
path: childPath,
|
|
113
|
+
name: basename(childPath),
|
|
114
|
+
branch: detectBranch(childPath),
|
|
115
|
+
projectType: detectProjectType(childPath),
|
|
116
|
+
});
|
|
117
|
+
} else if (depth < maxDepth) {
|
|
118
|
+
// Not a git repo — keep scanning deeper
|
|
119
|
+
scan(childPath, depth + 1);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
scan(cwd, 1);
|
|
125
|
+
|
|
126
|
+
// Sort alphabetically for consistent ordering
|
|
127
|
+
repos.sort((a, b) => a.name.localeCompare(b.name));
|
|
128
|
+
return repos;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Build a text description of the workspace layout for system prompt injection.
|
|
133
|
+
*/
|
|
134
|
+
function buildWorkspaceContext(workspace: Workspace): string {
|
|
135
|
+
if (!workspace.multiRepo || !workspace.repositories?.length) {
|
|
136
|
+
// Single-repo context
|
|
137
|
+
const lines = [
|
|
138
|
+
`## Workspace`,
|
|
139
|
+
``,
|
|
140
|
+
`- **Root**: \`${workspace.root}\``,
|
|
141
|
+
`- **Project type**: ${workspace.projectType}`,
|
|
142
|
+
];
|
|
143
|
+
if (workspace.gitBranch) {
|
|
144
|
+
lines.push(`- **Branch**: \`${workspace.gitBranch}\``);
|
|
145
|
+
}
|
|
146
|
+
return lines.join("\n");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Multi-repo context
|
|
150
|
+
const lines = [
|
|
151
|
+
`## Workspace (Multi-Repository)`,
|
|
152
|
+
``,
|
|
153
|
+
`You are working in a workspace that contains **${workspace.repositories.length} repositories**.`,
|
|
154
|
+
`The working directory is \`${workspace.cwd}\` which is NOT itself a git repo — it's a parent folder containing multiple repos.`,
|
|
155
|
+
``,
|
|
156
|
+
`When making changes, be aware of which repository a file belongs to. You can make coordinated changes across multiple repos.`,
|
|
157
|
+
``,
|
|
158
|
+
`### Repositories`,
|
|
159
|
+
``,
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
for (const repo of workspace.repositories) {
|
|
163
|
+
const branch = repo.branch ? ` (branch: \`${repo.branch}\`)` : "";
|
|
164
|
+
const type = repo.projectType !== "unknown" ? ` [${repo.projectType}]` : "";
|
|
165
|
+
lines.push(`- **${repo.name}**${type}: \`${repo.path}\`${branch}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return lines.join("\n");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function workspaceExtension(
|
|
172
|
+
opts: WorkspaceExtensionOptions = {},
|
|
173
|
+
): Extension {
|
|
174
|
+
let workspace: Workspace | undefined;
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
name: "workspace",
|
|
178
|
+
|
|
179
|
+
init(actx: AgentContext) {
|
|
180
|
+
const cwd = resolve(opts.cwd ?? process.cwd());
|
|
181
|
+
const maxDepth = opts.maxDepth ?? 2;
|
|
182
|
+
const gitRoot = detectGitRoot(cwd);
|
|
183
|
+
|
|
184
|
+
if (gitRoot !== null) {
|
|
185
|
+
// Single-repo mode: cwd is inside a git repo
|
|
186
|
+
const dexDir = join(gitRoot, ".dex");
|
|
187
|
+
const gitBranch = detectBranch(cwd);
|
|
188
|
+
const projectType = detectProjectType(gitRoot);
|
|
189
|
+
|
|
190
|
+
if (!existsSync(dexDir)) {
|
|
191
|
+
mkdirSync(dexDir, { recursive: true });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
workspace = {
|
|
195
|
+
cwd,
|
|
196
|
+
root: gitRoot,
|
|
197
|
+
dexDir,
|
|
198
|
+
projectType,
|
|
199
|
+
multiRepo: false,
|
|
200
|
+
...(gitBranch !== undefined ? { gitBranch } : {}),
|
|
201
|
+
};
|
|
202
|
+
} else {
|
|
203
|
+
// Not in a git repo — scan for child repos (multi-repo mode)
|
|
204
|
+
const repositories = discoverRepos(cwd, maxDepth);
|
|
205
|
+
const multiRepo = repositories.length > 0;
|
|
206
|
+
const dexDir = join(cwd, ".dex");
|
|
207
|
+
|
|
208
|
+
if (!existsSync(dexDir)) {
|
|
209
|
+
mkdirSync(dexDir, { recursive: true });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
workspace = {
|
|
213
|
+
cwd,
|
|
214
|
+
root: cwd,
|
|
215
|
+
dexDir,
|
|
216
|
+
projectType: detectProjectType(cwd),
|
|
217
|
+
multiRepo,
|
|
218
|
+
...(multiRepo ? { repositories } : {}),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
actx.state.set("workspace", workspace);
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
on: {
|
|
226
|
+
"session-start"(actx: AgentContext) {
|
|
227
|
+
if (!workspace) return;
|
|
228
|
+
const text = buildWorkspaceContext(workspace);
|
|
229
|
+
const content: Content = { type: "text", text };
|
|
230
|
+
return (async function* () {
|
|
231
|
+
yield content;
|
|
232
|
+
})();
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @dex-ai/coding-agent-sdk — coding agent extensions + factory.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { CodingAgent } from "./create";
|
|
6
|
+
export { envExtension } from "./extensions/env";
|
|
7
|
+
export type { EnvExtensionOptions, EnvState } from "./extensions/env";
|
|
8
|
+
export type {
|
|
9
|
+
CodingAgentOptions,
|
|
10
|
+
CodingAgentConfig,
|
|
11
|
+
Workspace,
|
|
12
|
+
Repository,
|
|
13
|
+
ProjectType,
|
|
14
|
+
ApprovalConfig,
|
|
15
|
+
ApprovalHandler,
|
|
16
|
+
ApprovalRequest,
|
|
17
|
+
ApprovalResult,
|
|
18
|
+
ToolCategory,
|
|
19
|
+
PermissionMode,
|
|
20
|
+
PermissionSettings,
|
|
21
|
+
} from "./types";
|
|
22
|
+
export {
|
|
23
|
+
settingsExtension,
|
|
24
|
+
loadGlobalSettings,
|
|
25
|
+
saveGlobalSettings,
|
|
26
|
+
addAllowedTool,
|
|
27
|
+
setPermissionMode,
|
|
28
|
+
setDefaultThinking,
|
|
29
|
+
saveProviderConfig,
|
|
30
|
+
setDefaultProvider,
|
|
31
|
+
saveLastUsedModel,
|
|
32
|
+
loadAgentState,
|
|
33
|
+
} from "./extensions/settings";
|
|
34
|
+
export type {
|
|
35
|
+
GlobalSettings,
|
|
36
|
+
SettingsExtensionState,
|
|
37
|
+
ProviderSettingsConfig,
|
|
38
|
+
AgentState,
|
|
39
|
+
} from "./extensions/settings";
|
|
40
|
+
export { listSessions } from "./extensions/session";
|
|
41
|
+
export type { SessionInfo } from "./extensions/session";
|
|
42
|
+
export { buildSystemPrompt } from "./extensions/system-prompt";
|
|
43
|
+
export type { SystemPromptOptions } from "./extensions/system-prompt";
|
|
44
|
+
export { tasksExtension } from "@dex-ai/core-extensions";
|
|
45
|
+
export type { Task, TaskStatus, TaskEvent } from "@dex-ai/core-extensions";
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for @dex-ai/coding-agent-sdk.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Extension } from "@dex-ai/sdk";
|
|
6
|
+
|
|
7
|
+
/* ------------------------------------------------------------------ */
|
|
8
|
+
/* Config */
|
|
9
|
+
/* ------------------------------------------------------------------ */
|
|
10
|
+
|
|
11
|
+
export interface CodingAgentConfig {
|
|
12
|
+
readonly provider: string;
|
|
13
|
+
readonly model: string;
|
|
14
|
+
readonly cwd: string;
|
|
15
|
+
readonly maxSteps?: number;
|
|
16
|
+
readonly maxTokens?: number;
|
|
17
|
+
readonly temperature?: number;
|
|
18
|
+
readonly approval?: ApprovalConfig;
|
|
19
|
+
readonly extensions?: ReadonlyArray<string>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ApprovalConfig {
|
|
23
|
+
/** Tools that never need approval. Default: ["read", "search"]. */
|
|
24
|
+
readonly safeTools?: ReadonlyArray<string>;
|
|
25
|
+
/** Regex patterns for bash commands that are auto-approved. */
|
|
26
|
+
readonly autoApprovePatterns?: ReadonlyArray<string>;
|
|
27
|
+
/** Approval mode. Default: "destructive". */
|
|
28
|
+
readonly mode?: "always" | "destructive" | "never";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* ------------------------------------------------------------------ */
|
|
32
|
+
/* Permission Mode */
|
|
33
|
+
/* ------------------------------------------------------------------ */
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Permission modes — controls how tool approvals are handled.
|
|
37
|
+
* - 'read': allow read tools, require one-time session approval for all others
|
|
38
|
+
* - 'auto': allow read tools + always-allowed tools, one-time session approval for rest
|
|
39
|
+
* - 'yolo': bypass all permissions except deny list
|
|
40
|
+
*/
|
|
41
|
+
export type PermissionMode = "read" | "auto" | "yolo";
|
|
42
|
+
|
|
43
|
+
export interface PermissionSettings {
|
|
44
|
+
/** Active permission mode. Default: 'auto'. */
|
|
45
|
+
readonly mode: PermissionMode;
|
|
46
|
+
/** Tools that are always allowed (auto-approved in 'auto' mode). */
|
|
47
|
+
readonly allowedTools?: ReadonlyArray<string>;
|
|
48
|
+
/** Tools that are always denied (blocked in 'yolo' mode). */
|
|
49
|
+
readonly deniedTools?: ReadonlyArray<string>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* ------------------------------------------------------------------ */
|
|
53
|
+
/* Workspace */
|
|
54
|
+
/* ------------------------------------------------------------------ */
|
|
55
|
+
|
|
56
|
+
export type ProjectType =
|
|
57
|
+
| "node"
|
|
58
|
+
| "python"
|
|
59
|
+
| "rust"
|
|
60
|
+
| "go"
|
|
61
|
+
| "java"
|
|
62
|
+
| "unknown";
|
|
63
|
+
|
|
64
|
+
export interface Repository {
|
|
65
|
+
/** Absolute path to the repository root. */
|
|
66
|
+
readonly path: string;
|
|
67
|
+
/** Directory name (basename). */
|
|
68
|
+
readonly name: string;
|
|
69
|
+
/** Current git branch. */
|
|
70
|
+
readonly branch?: string | undefined;
|
|
71
|
+
/** Detected project type. */
|
|
72
|
+
readonly projectType: ProjectType;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface Workspace {
|
|
76
|
+
/** Working directory. */
|
|
77
|
+
readonly cwd: string;
|
|
78
|
+
/** Git root (or cwd if no git). */
|
|
79
|
+
readonly root: string;
|
|
80
|
+
/** .dex/ directory path. */
|
|
81
|
+
readonly dexDir: string;
|
|
82
|
+
/** Detected project type (of root or primary repo). */
|
|
83
|
+
readonly projectType: ProjectType;
|
|
84
|
+
/** Current git branch (single-repo mode). */
|
|
85
|
+
readonly gitBranch?: string | undefined;
|
|
86
|
+
/** Whether we're in multi-repo mode (cwd contains multiple git repos). */
|
|
87
|
+
readonly multiRepo: boolean;
|
|
88
|
+
/** Child repositories (multi-repo mode only). */
|
|
89
|
+
readonly repositories?: ReadonlyArray<Repository> | undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* ------------------------------------------------------------------ */
|
|
93
|
+
/* Approval */
|
|
94
|
+
/* ------------------------------------------------------------------ */
|
|
95
|
+
|
|
96
|
+
export type ToolCategory = "safe" | "write" | "execute" | "dangerous";
|
|
97
|
+
|
|
98
|
+
export interface ApprovalRequest {
|
|
99
|
+
readonly toolName: string;
|
|
100
|
+
readonly toolCallId: string;
|
|
101
|
+
readonly input: unknown;
|
|
102
|
+
readonly category: ToolCategory;
|
|
103
|
+
readonly reason: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export type ApprovalResult =
|
|
107
|
+
| { readonly decision: "allow" }
|
|
108
|
+
| { readonly decision: "deny"; readonly reason?: string }
|
|
109
|
+
| { readonly decision: "allow-session" }
|
|
110
|
+
| { readonly decision: "allow-always" };
|
|
111
|
+
|
|
112
|
+
export type ApprovalHandler = (
|
|
113
|
+
request: ApprovalRequest,
|
|
114
|
+
) => Promise<ApprovalResult>;
|
|
115
|
+
|
|
116
|
+
/* ------------------------------------------------------------------ */
|
|
117
|
+
/* CodingAgent options */
|
|
118
|
+
/* ------------------------------------------------------------------ */
|
|
119
|
+
|
|
120
|
+
export interface CodingAgentOptions {
|
|
121
|
+
/** Provider extension name. */
|
|
122
|
+
readonly provider: string;
|
|
123
|
+
/** Model ID. */
|
|
124
|
+
readonly model: string;
|
|
125
|
+
/** Provider extension instance (e.g. openaiExtension()). */
|
|
126
|
+
readonly providerExtension: Extension;
|
|
127
|
+
/** Additional provider extensions (for multi-provider model switching). */
|
|
128
|
+
readonly providerExtensions?: ReadonlyArray<Extension> | undefined;
|
|
129
|
+
/** Working directory. Default: process.cwd(). */
|
|
130
|
+
readonly cwd?: string | undefined;
|
|
131
|
+
/** Additional root directories for the sandbox. The agent can `cd` into any of these. */
|
|
132
|
+
readonly rootDirs?: string[] | undefined;
|
|
133
|
+
/** Override the default system prompt. If omitted, uses the built-in Dex prompt. */
|
|
134
|
+
readonly systemPrompt?: string | undefined;
|
|
135
|
+
/** Explicit config overrides. */
|
|
136
|
+
readonly config?: Partial<CodingAgentConfig> | undefined;
|
|
137
|
+
/** Additional extensions (SDK or CLI unified). */
|
|
138
|
+
readonly extensions?: ReadonlyArray<Extension> | undefined;
|
|
139
|
+
/** Approval handler — provided by the CLI host or consumer. */
|
|
140
|
+
readonly onApproval?: ApprovalHandler | undefined;
|
|
141
|
+
/** Resume an existing session. */
|
|
142
|
+
readonly sessionId?: string | undefined;
|
|
143
|
+
/** Session storage directory. Default: .dex/sessions/. */
|
|
144
|
+
readonly sessionDir?: string | undefined;
|
|
145
|
+
|
|
146
|
+
/** Seed messages to restore conversation history (e.g. on reload). */
|
|
147
|
+
readonly messages?: ReadonlyArray<import("@dex-ai/sdk").Message> | undefined;
|
|
148
|
+
}
|