tokenmaw 0.3.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/README.md +150 -0
- package/agents/coordinator.md +13 -0
- package/agents/explorer.md +21 -0
- package/agents/implement.md +23 -0
- package/agents/main.md +25 -0
- package/agents/review.md +21 -0
- package/dist/backend.js +595 -0
- package/dist/cli.js +101 -0
- package/dist/config.js +155 -0
- package/dist/diff.js +45 -0
- package/dist/domain/agent.js +1 -0
- package/dist/fetch.js +110 -0
- package/dist/infra/file-snapshot.js +54 -0
- package/dist/infra/tools.js +1300 -0
- package/dist/markdown.js +274 -0
- package/dist/model-config.js +48 -0
- package/dist/policy.js +80 -0
- package/dist/responses.js +81 -0
- package/dist/runtime/agent-registry.js +139 -0
- package/dist/runtime/agent-runtime.js +993 -0
- package/dist/runtime/agent-store.js +152 -0
- package/dist/runtime/locks.js +46 -0
- package/dist/runtime/session-timeline.js +92 -0
- package/dist/tools/index.js +4 -0
- package/dist/tools/registry.js +51 -0
- package/dist/tools/types.js +1 -0
- package/dist/ui/clipboard.js +24 -0
- package/dist/ui/commands.js +20 -0
- package/dist/ui/composer-layout.js +31 -0
- package/dist/ui/fullscreen-tui.js +1405 -0
- package/dist/ui/markdown.js +81 -0
- package/dist/ui/syntax.js +17 -0
- package/dist/ui/tui-design.js +94 -0
- package/dist/ui/welcome.js +24 -0
- package/dist/version.js +4 -0
- package/docs/architecture-revision.md +281 -0
- package/package.json +47 -0
- package/skills/debugging.md +18 -0
- package/skills/git-workflow.md +14 -0
- package/skills/node-express.md +27 -0
- package/skills/python-flask.md +22 -0
- package/skills/react-component.md +24 -0
- package/skills/sql-database.md +18 -0
- package/skills/testing.md +12 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config.ts — .agentrc config file support.
|
|
3
|
+
*
|
|
4
|
+
* Merges .agentrc from the current project and user home. User values win,
|
|
5
|
+
* and interactive changes are persisted to the user-scoped file.
|
|
6
|
+
*
|
|
7
|
+
* Format: JSON with optional fields:
|
|
8
|
+
* {
|
|
9
|
+
* "baseUrl": "http://localhost:11434",
|
|
10
|
+
* "model": "gemma4:31b-cloud",
|
|
11
|
+
* "backend": "openai",
|
|
12
|
+
* "apiKey": "sk-...",
|
|
13
|
+
* "artifactsDir": ".agent-workspace/artifacts",
|
|
14
|
+
* "models": {
|
|
15
|
+
* "fast": {
|
|
16
|
+
* "model": "provider-model-id",
|
|
17
|
+
* "requestOptions": {
|
|
18
|
+
* "extraBody": {
|
|
19
|
+
* "provider_option": true
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
* }
|
|
25
|
+
*/
|
|
26
|
+
import { readFile, writeFile, mkdir, rename, rm } from 'node:fs/promises';
|
|
27
|
+
import { dirname, join, resolve } from 'node:path';
|
|
28
|
+
import { homedir } from 'node:os';
|
|
29
|
+
import { randomUUID } from 'node:crypto';
|
|
30
|
+
const CONFIG_FILES = ['.agentrc', '.agentrc.json'];
|
|
31
|
+
const configWrites = new Map();
|
|
32
|
+
async function replaceConfigFile(tempPath, path) {
|
|
33
|
+
let lastError;
|
|
34
|
+
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
35
|
+
try {
|
|
36
|
+
await rename(tempPath, path);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
lastError = error;
|
|
41
|
+
const code = error.code;
|
|
42
|
+
if (code !== 'EPERM' && code !== 'EEXIST' && code !== 'EACCES')
|
|
43
|
+
throw error;
|
|
44
|
+
await rm(path, { force: true }).catch(() => undefined);
|
|
45
|
+
await new Promise((resolveP) => setTimeout(resolveP, (attempt + 1) * 4));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
throw lastError;
|
|
49
|
+
}
|
|
50
|
+
function configHome() {
|
|
51
|
+
return process.env.CODER_CONFIG_HOME?.trim() || homedir();
|
|
52
|
+
}
|
|
53
|
+
function mergeConfig(project, user) {
|
|
54
|
+
return {
|
|
55
|
+
...project,
|
|
56
|
+
...user,
|
|
57
|
+
models: Object.keys(project.models ?? {}).length || Object.keys(user.models ?? {}).length
|
|
58
|
+
? { ...(project.models ?? {}), ...(user.models ?? {}) }
|
|
59
|
+
: undefined,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function parseConfig(raw) {
|
|
63
|
+
const parsed = JSON.parse(raw);
|
|
64
|
+
// roleModels belonged to the retired hard-coded Reception/Brain/Worker
|
|
65
|
+
// architecture. Agent-specific model selection now lives in Agent Specs.
|
|
66
|
+
delete parsed.roleModels;
|
|
67
|
+
if (parsed.backend && !['ollama', 'openai', 'anthropic'].includes(parsed.backend)) {
|
|
68
|
+
throw new Error(`Invalid backend "${parsed.backend}" in config. Use "openai", "anthropic", or "ollama".`);
|
|
69
|
+
}
|
|
70
|
+
if (parsed.artifactsDir !== undefined && typeof parsed.artifactsDir !== 'string') {
|
|
71
|
+
throw new Error('Invalid artifactsDir in config. Use a string path.');
|
|
72
|
+
}
|
|
73
|
+
if (parsed.models) {
|
|
74
|
+
for (const [name, modelConfig] of Object.entries(parsed.models)) {
|
|
75
|
+
if (modelConfig.wireApi && !['chat', 'responses'].includes(modelConfig.wireApi)) {
|
|
76
|
+
throw new Error(`Invalid wireApi for model alias "${name}". Use "chat" or "responses".`);
|
|
77
|
+
}
|
|
78
|
+
if (!modelConfig?.model || typeof modelConfig.model !== 'string') {
|
|
79
|
+
throw new Error(`Invalid model alias "${name}" in config. Each model alias needs a string "model".`);
|
|
80
|
+
}
|
|
81
|
+
if (modelConfig.backend && !['ollama', 'openai', 'anthropic'].includes(modelConfig.backend)) {
|
|
82
|
+
throw new Error(`Invalid backend "${modelConfig.backend}" for model alias "${name}". Use "openai", "anthropic", or "ollama".`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return parsed;
|
|
87
|
+
}
|
|
88
|
+
async function tryReadConfigWithPath(dir) {
|
|
89
|
+
for (const name of CONFIG_FILES) {
|
|
90
|
+
const path = join(dir, name);
|
|
91
|
+
try {
|
|
92
|
+
const raw = await readFile(path, 'utf8');
|
|
93
|
+
return { config: parseConfig(raw), path };
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (error.code !== 'ENOENT') {
|
|
97
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
98
|
+
throw new Error(`Could not load config ${path}: ${message}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Async config loader — call once at startup.
|
|
106
|
+
* CWD config takes precedence over home directory config.
|
|
107
|
+
*/
|
|
108
|
+
export async function loadConfig() {
|
|
109
|
+
const loaded = await loadConfigWithPath();
|
|
110
|
+
return loaded.config;
|
|
111
|
+
}
|
|
112
|
+
export async function loadConfigWithPath() {
|
|
113
|
+
const userDir = configHome();
|
|
114
|
+
const sameDir = resolve(process.cwd()).toLowerCase() === resolve(userDir).toLowerCase();
|
|
115
|
+
const projectConfig = sameDir ? null : await tryReadConfigWithPath(process.cwd());
|
|
116
|
+
const userConfig = await tryReadConfigWithPath(userDir);
|
|
117
|
+
return {
|
|
118
|
+
config: mergeConfig(projectConfig?.config ?? {}, userConfig?.config ?? {}),
|
|
119
|
+
path: userConfig?.path ?? join(userDir, '.agentrc'),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
export async function saveSelectedModel(model) {
|
|
123
|
+
const loaded = await loadConfigWithPath();
|
|
124
|
+
const nextConfig = {
|
|
125
|
+
...loaded.config,
|
|
126
|
+
model,
|
|
127
|
+
};
|
|
128
|
+
return saveConfig(nextConfig, loaded.path);
|
|
129
|
+
}
|
|
130
|
+
export async function saveConfig(config, existingPath) {
|
|
131
|
+
const path = existingPath ?? join(configHome(), '.agentrc');
|
|
132
|
+
const payload = `${JSON.stringify(config, null, 2)}\n`;
|
|
133
|
+
const previous = configWrites.get(path) ?? Promise.resolve();
|
|
134
|
+
const next = previous.catch(() => undefined).then(async () => {
|
|
135
|
+
await mkdir(dirname(path), { recursive: true });
|
|
136
|
+
const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
137
|
+
try {
|
|
138
|
+
await writeFile(tempPath, payload, 'utf8');
|
|
139
|
+
await replaceConfigFile(tempPath, path);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
await rm(tempPath, { force: true }).catch(() => undefined);
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
configWrites.set(path, next);
|
|
147
|
+
try {
|
|
148
|
+
await next;
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
if (configWrites.get(path) === next)
|
|
152
|
+
configWrites.delete(path);
|
|
153
|
+
}
|
|
154
|
+
return path;
|
|
155
|
+
}
|
package/dist/diff.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export function unifiedDiff(filePath, before, after, maxChangedLines = 160) {
|
|
2
|
+
if (before === after)
|
|
3
|
+
return '';
|
|
4
|
+
const beforeLines = before.split('\n');
|
|
5
|
+
const afterLines = after.split('\n');
|
|
6
|
+
let start = 0;
|
|
7
|
+
while (start < beforeLines.length && start < afterLines.length && beforeLines[start] === afterLines[start]) {
|
|
8
|
+
start += 1;
|
|
9
|
+
}
|
|
10
|
+
let beforeEnd = beforeLines.length - 1;
|
|
11
|
+
let afterEnd = afterLines.length - 1;
|
|
12
|
+
while (beforeEnd >= start && afterEnd >= start && beforeLines[beforeEnd] === afterLines[afterEnd]) {
|
|
13
|
+
beforeEnd -= 1;
|
|
14
|
+
afterEnd -= 1;
|
|
15
|
+
}
|
|
16
|
+
const contextBeforeStart = Math.max(0, start - 3);
|
|
17
|
+
const contextAfterEnd = Math.min(afterLines.length - 1, afterEnd + 3);
|
|
18
|
+
const lines = [
|
|
19
|
+
'```diff',
|
|
20
|
+
`--- ${filePath}`,
|
|
21
|
+
`+++ ${filePath}`,
|
|
22
|
+
`@@ -${contextBeforeStart + 1} +${contextBeforeStart + 1} @@`,
|
|
23
|
+
];
|
|
24
|
+
for (const line of beforeLines.slice(contextBeforeStart, start)) {
|
|
25
|
+
lines.push(` ${line}`);
|
|
26
|
+
}
|
|
27
|
+
const removed = beforeLines.slice(start, beforeEnd + 1);
|
|
28
|
+
const added = afterLines.slice(start, afterEnd + 1);
|
|
29
|
+
const changed = [
|
|
30
|
+
...removed.map((line) => `-${line}`),
|
|
31
|
+
...added.map((line) => `+${line}`),
|
|
32
|
+
];
|
|
33
|
+
if (changed.length > maxChangedLines) {
|
|
34
|
+
lines.push(...changed.slice(0, maxChangedLines));
|
|
35
|
+
lines.push(` ... ${changed.length - maxChangedLines} more changed lines`);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
lines.push(...changed);
|
|
39
|
+
}
|
|
40
|
+
for (const line of afterLines.slice(afterEnd + 1, contextAfterEnd + 1)) {
|
|
41
|
+
lines.push(` ${line}`);
|
|
42
|
+
}
|
|
43
|
+
lines.push('```');
|
|
44
|
+
return lines.join('\n');
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/fetch.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fetch.ts — Resilient HTTP client with retry, timeout, and reconnection.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the native `fetch` with:
|
|
5
|
+
* • Configurable retry count and backoff
|
|
6
|
+
* • Request timeout
|
|
7
|
+
* • Connection-refused detection for local LLM servers
|
|
8
|
+
*/
|
|
9
|
+
export class FetchError extends Error {
|
|
10
|
+
status;
|
|
11
|
+
retriable;
|
|
12
|
+
constructor(message, status, retriable) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.status = status;
|
|
15
|
+
this.retriable = retriable;
|
|
16
|
+
this.name = 'FetchError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function isRetriable(status, error) {
|
|
20
|
+
if (status === null)
|
|
21
|
+
return true; // network error
|
|
22
|
+
if (status === 429)
|
|
23
|
+
return true; // rate limited
|
|
24
|
+
if (status >= 500)
|
|
25
|
+
return true; // server error
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
function getErrorMessage(status, cause) {
|
|
29
|
+
if (cause instanceof Error && 'code' in cause) {
|
|
30
|
+
const code = cause.code;
|
|
31
|
+
if (code === 'ECONNREFUSED') {
|
|
32
|
+
return 'Connection refused — is the configured LLM server running? (LLM_BASE_URL / OLLAMA_BASE_URL)';
|
|
33
|
+
}
|
|
34
|
+
if (code === 'ENOTFOUND') {
|
|
35
|
+
return 'DNS lookup failed — check LLM_BASE_URL';
|
|
36
|
+
}
|
|
37
|
+
if (code === 'ECONNRESET') {
|
|
38
|
+
return 'Connection reset by server';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (status !== null)
|
|
42
|
+
return `HTTP ${status}`;
|
|
43
|
+
return String(cause);
|
|
44
|
+
}
|
|
45
|
+
const MAX_ERROR_BODY_LENGTH = 512;
|
|
46
|
+
async function errorResponseBody(response) {
|
|
47
|
+
try {
|
|
48
|
+
const text = (await response.text()).trim();
|
|
49
|
+
if (!text)
|
|
50
|
+
return '';
|
|
51
|
+
return text.length > MAX_ERROR_BODY_LENGTH ? `${text.slice(0, MAX_ERROR_BODY_LENGTH)}…` : text;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return '';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Resilient fetch with retry, timeout, and backoff.
|
|
59
|
+
*/
|
|
60
|
+
export async function resilientFetch(url, opts = {}) {
|
|
61
|
+
const { retries = 3, retryDelay = 1000, timeout = 120_000, ...fetchOpts } = opts;
|
|
62
|
+
let lastError = null;
|
|
63
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
64
|
+
const controller = new AbortController();
|
|
65
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
66
|
+
// Merge user-provided signal with our timeout signal
|
|
67
|
+
if (fetchOpts.signal) {
|
|
68
|
+
fetchOpts.signal.addEventListener('abort', () => controller.abort());
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const response = await fetch(url, {
|
|
72
|
+
...fetchOpts,
|
|
73
|
+
signal: controller.signal,
|
|
74
|
+
});
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
const retriable = isRetriable(response.status, null);
|
|
78
|
+
if (retriable && attempt < retries) {
|
|
79
|
+
const delay = retryDelay * Math.pow(2, attempt);
|
|
80
|
+
await sleep(delay);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const body = await errorResponseBody(response);
|
|
84
|
+
throw new FetchError(getErrorMessage(response.status, null) + (body ? `: ${body}` : ''), response.status, false);
|
|
85
|
+
}
|
|
86
|
+
return response;
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
if (err instanceof FetchError)
|
|
91
|
+
throw err;
|
|
92
|
+
// Caller cancellation is intentional, not a transient network failure.
|
|
93
|
+
// Never retry an aborted model request.
|
|
94
|
+
if (fetchOpts.signal?.aborted) {
|
|
95
|
+
throw new FetchError('Request aborted', null, false);
|
|
96
|
+
}
|
|
97
|
+
const retriable = isRetriable(null, err);
|
|
98
|
+
lastError = new FetchError(getErrorMessage(null, err), null, retriable);
|
|
99
|
+
if (!retriable || attempt >= retries) {
|
|
100
|
+
throw lastError;
|
|
101
|
+
}
|
|
102
|
+
const delay = retryDelay * Math.pow(2, attempt);
|
|
103
|
+
await sleep(delay);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
throw lastError ?? new FetchError('Unknown fetch error', null, false);
|
|
107
|
+
}
|
|
108
|
+
function sleep(ms) {
|
|
109
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
110
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { join, relative, resolve } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
function sanitizeComponent(name) {
|
|
6
|
+
return name.replace(/[^a-zA-Z0-9._-]+/g, '_').replace(/^_+|_+$/g, '') || 'root';
|
|
7
|
+
}
|
|
8
|
+
function externalKey(abs) {
|
|
9
|
+
return sanitizeComponent(abs.replace(/^([a-zA-Z]:)?[/\\]+/, ''));
|
|
10
|
+
}
|
|
11
|
+
function snapshotDirFor(abs, workspaceRoot) {
|
|
12
|
+
if (workspaceRoot)
|
|
13
|
+
return resolve(workspaceRoot, '.coder', 'snapshots');
|
|
14
|
+
return join(tmpdir(), 'coder-snapshots', externalKey(abs));
|
|
15
|
+
}
|
|
16
|
+
function snapshotNameFor(abs, workspaceRoot) {
|
|
17
|
+
const relKey = workspaceRoot
|
|
18
|
+
? sanitizeComponent(relative(workspaceRoot, abs).replace(/\\/g, '/'))
|
|
19
|
+
: externalKey(abs);
|
|
20
|
+
const stamp = new Date().toISOString().replace(/[^a-zA-Z0-9]+/g, '');
|
|
21
|
+
return `${relKey}~${stamp}~${randomBytes(4).toString('hex')}.bak`;
|
|
22
|
+
}
|
|
23
|
+
function errorCode(error) {
|
|
24
|
+
const code = error.code;
|
|
25
|
+
return code ?? 'error';
|
|
26
|
+
}
|
|
27
|
+
export async function snapshotBeforeWrite(filePath, workspaceRoot) {
|
|
28
|
+
try {
|
|
29
|
+
const abs = resolve(filePath);
|
|
30
|
+
const info = await stat(abs).catch(() => undefined);
|
|
31
|
+
if (!info || !info.isFile())
|
|
32
|
+
return { path: null };
|
|
33
|
+
let content;
|
|
34
|
+
try {
|
|
35
|
+
content = await readFile(abs, 'utf8');
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
return { path: null, reason: `could not read existing file (${errorCode(error)})` };
|
|
39
|
+
}
|
|
40
|
+
const dir = snapshotDirFor(abs, workspaceRoot);
|
|
41
|
+
const snapshotPath = join(dir, snapshotNameFor(abs, workspaceRoot));
|
|
42
|
+
try {
|
|
43
|
+
await mkdir(dir, { recursive: true });
|
|
44
|
+
await writeFile(snapshotPath, content, 'utf8');
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
return { path: null, reason: `could not write snapshot (${errorCode(error)})` };
|
|
48
|
+
}
|
|
49
|
+
return { path: snapshotPath };
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return { path: null, reason: `snapshot failed (${String(error?.message ?? error)})` };
|
|
53
|
+
}
|
|
54
|
+
}
|