@twinklerg/coden 0.1.0
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/LICENSE +21 -0
- package/README.md +219 -0
- package/dist/index.js +21269 -0
- package/package.json +48 -0
- package/src/cli/agent-command.ts +497 -0
- package/src/cli/format.ts +42 -0
- package/src/cli/index.ts +149 -0
- package/src/cli/plugin-command.ts +217 -0
- package/src/config/config.ts +96 -0
- package/src/config/trust.ts +35 -0
- package/src/context/manager.ts +186 -0
- package/src/context/truncate.ts +9 -0
- package/src/core/events.ts +32 -0
- package/src/core/runtime.ts +402 -0
- package/src/core/types.ts +97 -0
- package/src/index.ts +14 -0
- package/src/observability/terminal.ts +201 -0
- package/src/observability/trace.ts +30 -0
- package/src/permissions/policy.ts +56 -0
- package/src/permissions/workspace.ts +139 -0
- package/src/plugins/api.ts +68 -0
- package/src/plugins/bun-package-manager.ts +35 -0
- package/src/plugins/installed-loader.ts +144 -0
- package/src/plugins/installer.ts +314 -0
- package/src/plugins/manifest.ts +89 -0
- package/src/plugins/package-manager.ts +10 -0
- package/src/plugins/package-metadata.ts +95 -0
- package/src/plugins/paths.ts +43 -0
- package/src/plugins/specifier.ts +63 -0
- package/src/plugins/transaction.ts +403 -0
- package/src/process/runner.ts +134 -0
- package/src/providers/anthropic.ts +117 -0
- package/src/providers/openai.ts +96 -0
- package/src/providers/scripted.ts +28 -0
- package/src/sessions/store.ts +278 -0
- package/src/tools/builtin/bash.ts +56 -0
- package/src/tools/builtin/edit.ts +42 -0
- package/src/tools/builtin/index.ts +9 -0
- package/src/tools/builtin/read.ts +91 -0
- package/src/tools/builtin/write.ts +34 -0
- package/src/tools/executor.ts +90 -0
- package/src/tools/plugin-loader.ts +122 -0
- package/src/tools/registry.ts +97 -0
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@twinklerg/coden",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A minimal event-driven coding agent",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"files": [
|
|
8
|
+
"src",
|
|
9
|
+
"dist/index.js",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"bin": {
|
|
13
|
+
"coden": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./src/index.ts",
|
|
17
|
+
"./plugin": "./src/plugins/api.ts"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"start": "bun run src/cli/index.ts",
|
|
21
|
+
"build": "mkdir -p dist && bun build src/cli/index.ts --target=node --outfile dist/index.js && sed -i.bak '1s|^#!.*|#!/usr/bin/env node|' dist/index.js && rm -f dist/index.js.bak",
|
|
22
|
+
"prepack": "bun run build",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"test:watch": "vitest",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"lint": "biome check .",
|
|
27
|
+
"format": "biome format --write ."
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@anthropic-ai/sdk": "^0.61.0",
|
|
31
|
+
"ajv": "^8.17.1",
|
|
32
|
+
"commander": "^14.0.0",
|
|
33
|
+
"openai": "^5.16.0",
|
|
34
|
+
"picocolors": "^1.1.1"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@biomejs/biome": "^2.2.2",
|
|
38
|
+
"@types/node": "^24.3.0",
|
|
39
|
+
"typescript": "^5.9.2",
|
|
40
|
+
"vitest": "^3.2.4"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"bun": ">=1.1.0"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { stdin, stdout } from "node:process";
|
|
3
|
+
import { createInterface, type Interface } from "node:readline/promises";
|
|
4
|
+
import { InvalidArgumentError } from "commander";
|
|
5
|
+
import {
|
|
6
|
+
type CodeNConfig,
|
|
7
|
+
loadConfig,
|
|
8
|
+
type ProviderName,
|
|
9
|
+
userConfigDir,
|
|
10
|
+
} from "../config/config.js";
|
|
11
|
+
import { TrustStore } from "../config/trust.js";
|
|
12
|
+
import { ContextManager } from "../context/manager.js";
|
|
13
|
+
import { EventBus } from "../core/events.js";
|
|
14
|
+
import { AgentRuntime } from "../core/runtime.js";
|
|
15
|
+
import type {
|
|
16
|
+
AgentMessage,
|
|
17
|
+
CodeNError,
|
|
18
|
+
ModelProvider,
|
|
19
|
+
ToolCall,
|
|
20
|
+
ToolDefinition,
|
|
21
|
+
ToolRisk,
|
|
22
|
+
} from "../core/types.js";
|
|
23
|
+
import { TerminalRenderer } from "../observability/terminal.js";
|
|
24
|
+
import { JSONLTraceWriter } from "../observability/trace.js";
|
|
25
|
+
import { type PermissionDecision, PermissionPolicy } from "../permissions/policy.js";
|
|
26
|
+
import { readWorkspaceTextFile } from "../permissions/workspace.js";
|
|
27
|
+
import {
|
|
28
|
+
InstalledPluginLoader,
|
|
29
|
+
type LoadedPackagePlugin,
|
|
30
|
+
type PackagePluginFailure,
|
|
31
|
+
} from "../plugins/installed-loader.js";
|
|
32
|
+
import { resolvePluginPaths } from "../plugins/paths.js";
|
|
33
|
+
import { PluginTransaction } from "../plugins/transaction.js";
|
|
34
|
+
import { AnthropicProvider } from "../providers/anthropic.js";
|
|
35
|
+
import { OpenAICompatibleProvider } from "../providers/openai.js";
|
|
36
|
+
import { SessionStore } from "../sessions/store.js";
|
|
37
|
+
import { builtinTools } from "../tools/builtin/index.js";
|
|
38
|
+
import { ToolExecutor } from "../tools/executor.js";
|
|
39
|
+
import { PluginLoader } from "../tools/plugin-loader.js";
|
|
40
|
+
import { ToolRegistry, type ToolSource } from "../tools/registry.js";
|
|
41
|
+
import { formatSessionList, renderResumeBanner } from "./format.js";
|
|
42
|
+
|
|
43
|
+
export interface AgentCommandOptions {
|
|
44
|
+
provider?: ProviderName;
|
|
45
|
+
model?: string;
|
|
46
|
+
resume?: string | boolean;
|
|
47
|
+
auto: boolean;
|
|
48
|
+
verbose: boolean;
|
|
49
|
+
maxSteps?: number;
|
|
50
|
+
plugin: string[];
|
|
51
|
+
print: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class ConfigError extends Error {}
|
|
55
|
+
|
|
56
|
+
async function loadConfigOrFail(
|
|
57
|
+
workspace: string,
|
|
58
|
+
cli: Parameters<typeof loadConfig>[1],
|
|
59
|
+
): Promise<CodeNConfig> {
|
|
60
|
+
try {
|
|
61
|
+
return await loadConfig(workspace, cli);
|
|
62
|
+
} catch (cause) {
|
|
63
|
+
throw new ConfigError(
|
|
64
|
+
`configuration: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
65
|
+
{ cause },
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function runAgentCommand(
|
|
71
|
+
initialPrompt: string | undefined,
|
|
72
|
+
options: AgentCommandOptions,
|
|
73
|
+
): Promise<void> {
|
|
74
|
+
const workspace = process.cwd();
|
|
75
|
+
const config = await loadConfigOrFail(workspace, {
|
|
76
|
+
...(options.provider ? { provider: options.provider } : {}),
|
|
77
|
+
...(options.model ? { model: options.model } : {}),
|
|
78
|
+
...(options.maxSteps ? { maxSteps: options.maxSteps } : {}),
|
|
79
|
+
plugins: options.plugin,
|
|
80
|
+
});
|
|
81
|
+
const events = new EventBus();
|
|
82
|
+
const needsInput = !options.auto || (!initialPrompt && !options.print);
|
|
83
|
+
const rl = needsInput ? createInterface({ input: stdin, output: process.stderr }) : undefined;
|
|
84
|
+
const resumedId = typeof options.resume === "string" ? options.resume : undefined;
|
|
85
|
+
const session = new SessionStore(config.dataDir, workspace, resumedId);
|
|
86
|
+
if (options.resume === true) {
|
|
87
|
+
rl?.close();
|
|
88
|
+
stdout.write(formatSessionList(await session.list()));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
let initialMessages: AgentMessage[] | undefined;
|
|
92
|
+
let recoveredSummary: string | undefined;
|
|
93
|
+
let recoveredCompactionEnd = 0;
|
|
94
|
+
let resumeBanner: string | undefined;
|
|
95
|
+
if (typeof options.resume === "string") {
|
|
96
|
+
const recovered = await session.recover();
|
|
97
|
+
initialMessages = recovered.messages;
|
|
98
|
+
recoveredSummary = recovered.summary;
|
|
99
|
+
recoveredCompactionEnd = recovered.compactionRange?.end ?? 0;
|
|
100
|
+
for (const warning of recovered.warnings) process.stderr.write(`coden: ${warning}\n`);
|
|
101
|
+
if (!options.print) resumeBanner = renderResumeBanner(session.sessionId, recovered.messages);
|
|
102
|
+
} else await session.create(workspace);
|
|
103
|
+
const trace = new JSONLTraceWriter(session.tracePath, events);
|
|
104
|
+
const renderer = new TerminalRenderer(events, {
|
|
105
|
+
verbose: options.verbose,
|
|
106
|
+
printMode: options.print,
|
|
107
|
+
});
|
|
108
|
+
let provider: ModelProvider;
|
|
109
|
+
try {
|
|
110
|
+
provider = createProvider(config.provider);
|
|
111
|
+
} catch (cause) {
|
|
112
|
+
throw new ConfigError(`provider: ${cause instanceof Error ? cause.message : String(cause)}`, {
|
|
113
|
+
cause,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const builtins = builtinTools();
|
|
117
|
+
const permissionPrompt = options.auto ? undefined : createPermissionPrompt(requireInterface(rl));
|
|
118
|
+
const permissions = new PermissionPolicy(options.auto, permissionPrompt);
|
|
119
|
+
const registry = new ToolRegistry(builtins);
|
|
120
|
+
const executor = new ToolExecutor(registry, permissions, events, workspace);
|
|
121
|
+
const trustStore = new TrustStore(path.join(userConfigDir(), "trusted-workspaces.json"));
|
|
122
|
+
const loader = new PluginLoader(builtins, events, options.auto, async (directory) => {
|
|
123
|
+
if (await trustStore.isTrusted(directory)) return true;
|
|
124
|
+
const allowed = await yesNo(
|
|
125
|
+
requireInterface(rl),
|
|
126
|
+
`Project plugins at ${directory} run with full process permissions. Trust? [y/N] `,
|
|
127
|
+
);
|
|
128
|
+
if (allowed) await trustStore.trust(directory);
|
|
129
|
+
return allowed;
|
|
130
|
+
});
|
|
131
|
+
const installedLoader = new InstalledPluginLoader();
|
|
132
|
+
const globalPaths = resolvePluginPaths(workspace, "global", config.dataDir);
|
|
133
|
+
const projectPaths = resolvePluginPaths(workspace, "project", config.dataDir);
|
|
134
|
+
const pluginDirs = [
|
|
135
|
+
{ path: path.join(userConfigDir(), "plugins"), project: false },
|
|
136
|
+
{ path: path.join(workspace, ".coden", "plugins"), project: true },
|
|
137
|
+
...config.plugins.map((item) => ({ path: path.resolve(workspace, item), project: true })),
|
|
138
|
+
];
|
|
139
|
+
const loadedPackageVersions = new Map<string, string>();
|
|
140
|
+
const loadInstalled = async () => {
|
|
141
|
+
await new PluginTransaction(globalPaths).recover();
|
|
142
|
+
await new PluginTransaction(projectPaths).recover();
|
|
143
|
+
const global = await loadInstalledScope(installedLoader, globalPaths, events, "global");
|
|
144
|
+
const projectTrusted = options.auto || (await trustStore.isWorkspaceTrusted(workspace));
|
|
145
|
+
const project = projectTrusted
|
|
146
|
+
? await loadInstalledScope(installedLoader, projectPaths, events, "project")
|
|
147
|
+
: { loaded: [], failed: [], unavailable: true };
|
|
148
|
+
if (project.unavailable) {
|
|
149
|
+
await events.emit("plugin.unavailable", {
|
|
150
|
+
source: "npm",
|
|
151
|
+
scope: "project",
|
|
152
|
+
path: projectPaths.root,
|
|
153
|
+
reason: "workspace is not trusted; run coden plugin install or sync after trusting",
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
const composed = await composeRuntimePackageRegistry(
|
|
157
|
+
builtins,
|
|
158
|
+
global.loaded,
|
|
159
|
+
project.loaded,
|
|
160
|
+
events,
|
|
161
|
+
);
|
|
162
|
+
for (const { scope, plugin } of composed.effective) {
|
|
163
|
+
const identity = `${scope}:${plugin.version}`;
|
|
164
|
+
const previous = loadedPackageVersions.get(plugin.packageName);
|
|
165
|
+
if (previous && previous !== identity) {
|
|
166
|
+
await events.emit("plugin.restart_required", {
|
|
167
|
+
source: "npm",
|
|
168
|
+
packageName: plugin.packageName,
|
|
169
|
+
loadedIdentity: previous,
|
|
170
|
+
diskIdentity: identity,
|
|
171
|
+
loadedVersion: previous.split(":").slice(1).join(":"),
|
|
172
|
+
diskVersion: plugin.version,
|
|
173
|
+
reason: "npm plugin metadata changed; restart CodeN to load it",
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
loadedPackageVersions.set(plugin.packageName, identity);
|
|
177
|
+
}
|
|
178
|
+
return { composed, global, project };
|
|
179
|
+
};
|
|
180
|
+
const reload = async () => {
|
|
181
|
+
const installed = await loadInstalled();
|
|
182
|
+
const loaded = await loader.load(pluginDirs, installed.composed.registry);
|
|
183
|
+
registry.replaceWith(loaded.registry);
|
|
184
|
+
return loaded;
|
|
185
|
+
};
|
|
186
|
+
await reload();
|
|
187
|
+
const projectInstructions = await readProjectInstructions(workspace);
|
|
188
|
+
const contextManager = new ContextManager({
|
|
189
|
+
contextWindow: config.contextWindow,
|
|
190
|
+
reservedOutputTokens: config.reservedOutputTokens,
|
|
191
|
+
safetyMargin: config.safetyMargin,
|
|
192
|
+
});
|
|
193
|
+
if (recoveredSummary) contextManager.setSummary(recoveredSummary, recoveredCompactionEnd);
|
|
194
|
+
const runtime = new AgentRuntime(
|
|
195
|
+
provider,
|
|
196
|
+
registry,
|
|
197
|
+
executor,
|
|
198
|
+
contextManager,
|
|
199
|
+
session,
|
|
200
|
+
events,
|
|
201
|
+
{
|
|
202
|
+
model: config.model,
|
|
203
|
+
maxSteps: config.maxSteps,
|
|
204
|
+
systemPrompt:
|
|
205
|
+
"You are CodeN, a concise coding agent. Inspect before editing, use tools carefully, and verify changes." +
|
|
206
|
+
(projectInstructions ? `\n\nProject instructions:\n${projectInstructions}` : ""),
|
|
207
|
+
},
|
|
208
|
+
initialMessages,
|
|
209
|
+
);
|
|
210
|
+
try {
|
|
211
|
+
if (initialPrompt) {
|
|
212
|
+
await runTurn(runtime, initialPrompt, rl);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (options.print) throw new Error("print mode requires a prompt");
|
|
216
|
+
await repl(runtime, session, reload, registry, requireInterface(rl), resumeBanner);
|
|
217
|
+
} finally {
|
|
218
|
+
rl?.close();
|
|
219
|
+
renderer.dispose();
|
|
220
|
+
await trace.flush();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
interface InstalledScopeResult {
|
|
225
|
+
loaded: LoadedPackagePlugin[];
|
|
226
|
+
failed: PackagePluginFailure[];
|
|
227
|
+
unavailable?: boolean;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export interface RuntimeEffectivePackage {
|
|
231
|
+
scope: "global" | "project";
|
|
232
|
+
plugin: LoadedPackagePlugin;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function composeRuntimePackageRegistry(
|
|
236
|
+
builtins: ToolDefinition[],
|
|
237
|
+
globalPlugins: LoadedPackagePlugin[],
|
|
238
|
+
projectPlugins: LoadedPackagePlugin[],
|
|
239
|
+
events: EventBus,
|
|
240
|
+
): Promise<{ registry: ToolRegistry; effective: RuntimeEffectivePackage[] }> {
|
|
241
|
+
const registry = new ToolRegistry(builtins);
|
|
242
|
+
const projectNames = new Set(projectPlugins.map((plugin) => plugin.packageName));
|
|
243
|
+
const packages: RuntimeEffectivePackage[] = [
|
|
244
|
+
...globalPlugins.map((plugin) => ({ scope: "global" as const, plugin })),
|
|
245
|
+
...projectPlugins.map((plugin) => ({ scope: "project" as const, plugin })),
|
|
246
|
+
];
|
|
247
|
+
const effective: RuntimeEffectivePackage[] = [];
|
|
248
|
+
for (const item of packages) {
|
|
249
|
+
if (item.scope === "global" && projectNames.has(item.plugin.packageName)) continue;
|
|
250
|
+
const candidate = registry.clone();
|
|
251
|
+
const source: ToolSource = {
|
|
252
|
+
kind: "npm",
|
|
253
|
+
pluginName: item.plugin.packageName,
|
|
254
|
+
pluginVersion: item.plugin.version,
|
|
255
|
+
path: item.plugin.entryPath,
|
|
256
|
+
};
|
|
257
|
+
try {
|
|
258
|
+
for (const tool of item.plugin.tools) candidate.register(tool, source);
|
|
259
|
+
registry.replaceWith(candidate);
|
|
260
|
+
effective.push(item);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
await events.emit("plugin.failed", {
|
|
263
|
+
source: "npm",
|
|
264
|
+
scope: item.scope,
|
|
265
|
+
packageName: item.plugin.packageName,
|
|
266
|
+
version: item.plugin.version,
|
|
267
|
+
path: item.plugin.entryPath,
|
|
268
|
+
message: `${error instanceof Error ? error.message : String(error)}; package skipped to preserve earlier tools`,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return { registry, effective };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export async function loadInstalledScope(
|
|
276
|
+
loader: InstalledPluginLoader,
|
|
277
|
+
paths: ReturnType<typeof resolvePluginPaths>,
|
|
278
|
+
events: EventBus,
|
|
279
|
+
scope: "global" | "project",
|
|
280
|
+
): Promise<InstalledScopeResult> {
|
|
281
|
+
try {
|
|
282
|
+
const result = await loader.loadScope(paths);
|
|
283
|
+
for (const plugin of result.loaded) {
|
|
284
|
+
await events.emit("plugin.loaded", {
|
|
285
|
+
source: "npm",
|
|
286
|
+
scope,
|
|
287
|
+
packageName: plugin.packageName,
|
|
288
|
+
version: plugin.version,
|
|
289
|
+
path: plugin.entryPath,
|
|
290
|
+
tools: plugin.tools.map((tool) => tool.name),
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
for (const failure of result.failed) {
|
|
294
|
+
await events.emit("plugin.failed", {
|
|
295
|
+
source: "npm",
|
|
296
|
+
scope,
|
|
297
|
+
packageName: failure.packageName,
|
|
298
|
+
path: failure.path,
|
|
299
|
+
message: `${failure.message}; run coden plugin sync to repair the runtime`,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
return result;
|
|
303
|
+
} catch (error) {
|
|
304
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
305
|
+
await events.emit("plugin.failed", {
|
|
306
|
+
source: "npm",
|
|
307
|
+
scope,
|
|
308
|
+
path: paths.runtimeDir,
|
|
309
|
+
message: `${message}; run coden plugin sync to repair the runtime`,
|
|
310
|
+
});
|
|
311
|
+
return { loaded: [], failed: [], unavailable: true };
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function readProjectInstructions(workspace: string): Promise<string> {
|
|
316
|
+
try {
|
|
317
|
+
return await readWorkspaceTextFile(workspace, "AGENTS.md");
|
|
318
|
+
} catch (error) {
|
|
319
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return "";
|
|
320
|
+
if ((error as CodeNError).category === "permission") {
|
|
321
|
+
process.stderr.write(`coden: ignoring AGENTS.md: ${(error as Error).message}\n`);
|
|
322
|
+
return "";
|
|
323
|
+
}
|
|
324
|
+
throw new ConfigError(
|
|
325
|
+
`project instructions: ${error instanceof Error ? error.message : String(error)}`,
|
|
326
|
+
{ cause: error },
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function createProvider(name: ProviderName): ModelProvider {
|
|
332
|
+
if (name === "anthropic") {
|
|
333
|
+
const apiKey = process.env.CODEN_ANTHROPIC_API_KEY;
|
|
334
|
+
if (!apiKey) throw new Error("CODEN_ANTHROPIC_API_KEY is required");
|
|
335
|
+
return new AnthropicProvider({ apiKey });
|
|
336
|
+
}
|
|
337
|
+
const apiKey = process.env.CODEN_OPENAI_API_KEY;
|
|
338
|
+
if (!apiKey) throw new Error("CODEN_OPENAI_API_KEY is required");
|
|
339
|
+
return new OpenAICompatibleProvider({
|
|
340
|
+
apiKey,
|
|
341
|
+
...(process.env.CODEN_OPENAI_BASE_URL ? { baseURL: process.env.CODEN_OPENAI_BASE_URL } : {}),
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function repl(
|
|
346
|
+
runtime: AgentRuntime,
|
|
347
|
+
session: SessionStore,
|
|
348
|
+
reload: () => Promise<{ loaded: string[]; failed: string[] }>,
|
|
349
|
+
registry: ToolRegistry,
|
|
350
|
+
rl: Interface,
|
|
351
|
+
resumeBanner?: string,
|
|
352
|
+
): Promise<void> {
|
|
353
|
+
stdout.write(CODEN_BANNER);
|
|
354
|
+
stdout.write(
|
|
355
|
+
resumeBanner
|
|
356
|
+
? `${resumeBanner}\nType /help for commands.\n`
|
|
357
|
+
: `CodeN session ${session.sessionId}. Type /help for commands.\n`,
|
|
358
|
+
);
|
|
359
|
+
while (true) {
|
|
360
|
+
const line = (await question(rl, "> ")).trim();
|
|
361
|
+
if (line === EOF) break;
|
|
362
|
+
if (!line) continue;
|
|
363
|
+
if (line === "/quit") break;
|
|
364
|
+
if (line === "/help") {
|
|
365
|
+
stdout.write("/help /session /sessions /compact /reload /new /quit\n");
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (line === "/sessions") {
|
|
369
|
+
stdout.write(formatSessionList(await session.list(), session.sessionId));
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (line === "/session") {
|
|
373
|
+
stdout.write(`${session.sessionId}\n`);
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (line === "/compact") {
|
|
377
|
+
await runtime.compact();
|
|
378
|
+
stdout.write("Context compacted.\n");
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (line === "/new") {
|
|
382
|
+
await runtime.reset();
|
|
383
|
+
stdout.write("Started a new conversation in this session.\n");
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
if (line === "/reload") {
|
|
387
|
+
const result = await reload();
|
|
388
|
+
stdout.write(
|
|
389
|
+
`Loaded: ${result.loaded.join(", ") || "none"}; failed: ${result.failed.length}; tools: ${registry
|
|
390
|
+
.list()
|
|
391
|
+
.map((tool) => tool.name)
|
|
392
|
+
.join(", ")}\n`,
|
|
393
|
+
);
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
await runTurn(runtime, line, rl);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function createPermissionPrompt(rl: Interface) {
|
|
401
|
+
return async (
|
|
402
|
+
tool: ToolDefinition,
|
|
403
|
+
call: ToolCall,
|
|
404
|
+
risk: ToolRisk,
|
|
405
|
+
signal?: AbortSignal,
|
|
406
|
+
): Promise<PermissionDecision> => {
|
|
407
|
+
const answer = await question(
|
|
408
|
+
rl,
|
|
409
|
+
`${risk.toUpperCase()} tool ${tool.name} ${JSON.stringify(call.input)}: [y]es/[s]ession/[N]o? `,
|
|
410
|
+
signal,
|
|
411
|
+
);
|
|
412
|
+
return answer.toLowerCase() === "y"
|
|
413
|
+
? "allow_once"
|
|
414
|
+
: answer.toLowerCase() === "s" && risk !== "dangerous"
|
|
415
|
+
? "allow_session"
|
|
416
|
+
: "deny";
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const EOF = "\u0004";
|
|
421
|
+
|
|
422
|
+
async function question(rl: Interface, message: string, signal?: AbortSignal): Promise<string> {
|
|
423
|
+
const local = signal ? undefined : new AbortController();
|
|
424
|
+
const activeSignal = signal ?? local?.signal;
|
|
425
|
+
const cancel = () => local?.abort(new Error("Cancelled by user"));
|
|
426
|
+
if (local) {
|
|
427
|
+
process.once("SIGINT", cancel);
|
|
428
|
+
rl.once("SIGINT", cancel);
|
|
429
|
+
}
|
|
430
|
+
try {
|
|
431
|
+
return activeSignal
|
|
432
|
+
? await rl.question(message, { signal: activeSignal })
|
|
433
|
+
: await rl.question(message);
|
|
434
|
+
} catch (error) {
|
|
435
|
+
if (activeSignal?.aborted) return "";
|
|
436
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
437
|
+
if (code === "ERR_USE_AFTER_CLOSE" || /readline was closed/.test((error as Error).message)) {
|
|
438
|
+
return EOF;
|
|
439
|
+
}
|
|
440
|
+
throw error;
|
|
441
|
+
} finally {
|
|
442
|
+
if (local) {
|
|
443
|
+
process.removeListener("SIGINT", cancel);
|
|
444
|
+
rl.removeListener("SIGINT", cancel);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function yesNo(rl: Interface, message: string): Promise<boolean> {
|
|
450
|
+
return /^y(?:es)?$/i.test(await question(rl, message));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function requireInterface(rl: Interface | undefined): Interface {
|
|
454
|
+
if (!rl) throw new Error("Interactive input is unavailable");
|
|
455
|
+
return rl;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function runTurn(runtime: AgentRuntime, text: string, rl?: Interface): Promise<void> {
|
|
459
|
+
const controller = new AbortController();
|
|
460
|
+
const cancel = () => controller.abort(new Error("Cancelled by user"));
|
|
461
|
+
process.once("SIGINT", cancel);
|
|
462
|
+
rl?.once("SIGINT", cancel);
|
|
463
|
+
try {
|
|
464
|
+
await runtime.run(text, controller.signal);
|
|
465
|
+
} catch (error) {
|
|
466
|
+
if (!controller.signal.aborted) throw error;
|
|
467
|
+
} finally {
|
|
468
|
+
process.removeListener("SIGINT", cancel);
|
|
469
|
+
rl?.removeListener("SIGINT", cancel);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export function parseProvider(value: string): ProviderName {
|
|
474
|
+
if (value !== "openai" && value !== "anthropic")
|
|
475
|
+
throw new InvalidArgumentError("must be openai or anthropic");
|
|
476
|
+
return value;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export function positiveInteger(value: string): number {
|
|
480
|
+
const parsed = Number(value);
|
|
481
|
+
if (!Number.isInteger(parsed) || parsed < 1)
|
|
482
|
+
throw new InvalidArgumentError("must be a positive integer");
|
|
483
|
+
return parsed;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export function collect(value: string, previous: string[]): string[] {
|
|
487
|
+
return [...previous, value];
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const CODEN_BANNER = `
|
|
491
|
+
██████╗ ██████╗ ██████╗ ███████╗███╗ ██╗
|
|
492
|
+
██╔════╝██╔═══██╗██╔══██╗██╔════╝████╗ ██║
|
|
493
|
+
██║ ██║ ██║██║ ██║█████╗ ██╔██╗ ██║
|
|
494
|
+
██║ ██║ ██║██║ ██║██╔══╝ ██║╚██╗██║
|
|
495
|
+
╚██████╗╚██████╔╝██████╔╝███████╗██║ ╚████║
|
|
496
|
+
╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝
|
|
497
|
+
`;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { AgentMessage } from "../core/types.js";
|
|
2
|
+
import type { SessionMeta } from "../sessions/store.js";
|
|
3
|
+
|
|
4
|
+
export function singleLine(text: string, max: number): string {
|
|
5
|
+
const one = text.replace(/\s+/g, " ").trim();
|
|
6
|
+
return one.length <= max ? one : `${one.slice(0, max - 1)}…`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function formatDateTime(iso: string): string {
|
|
10
|
+
const date = new Date(iso);
|
|
11
|
+
if (Number.isNaN(date.getTime())) return iso;
|
|
12
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
13
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function formatSessionList(sessions: SessionMeta[], currentId?: string): string {
|
|
17
|
+
if (sessions.length === 0) return "No sessions found.\n";
|
|
18
|
+
const lines = sessions.map((item) => {
|
|
19
|
+
const title = item.title ? singleLine(item.title, 40) : "(no title)";
|
|
20
|
+
const meta = item.messageCount === 0 ? "new session" : `${item.messageCount} messages`;
|
|
21
|
+
const active = item.id === currentId ? " *" : "";
|
|
22
|
+
return `${item.id}${active} ${title} (${meta}, ${formatDateTime(item.lastActivity)})`;
|
|
23
|
+
});
|
|
24
|
+
const header = currentId ? `Current session: ${currentId}\n` : "";
|
|
25
|
+
return `${lines.join("\n")}\n${header}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function renderResumeBanner(sessionId: string, messages: AgentMessage[]): string {
|
|
29
|
+
const count = messages.length;
|
|
30
|
+
const preview = messages
|
|
31
|
+
.filter((message) => message.role === "user" || message.role === "assistant")
|
|
32
|
+
.slice(-3);
|
|
33
|
+
const lines = [
|
|
34
|
+
`Resumed session ${sessionId} (${count} messages).`,
|
|
35
|
+
`Showing last ${preview.length} of ${count} messages.`,
|
|
36
|
+
];
|
|
37
|
+
for (const message of preview) {
|
|
38
|
+
const role = message.role === "user" ? "user" : "assistant";
|
|
39
|
+
lines.push(`┌ ${role.padEnd(9)} ${singleLine(message.content, 120)}`);
|
|
40
|
+
}
|
|
41
|
+
return lines.join("\n");
|
|
42
|
+
}
|