ashlrcode 1.0.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 +295 -0
- package/package.json +46 -0
- package/src/__tests__/branded-types.test.ts +47 -0
- package/src/__tests__/context.test.ts +163 -0
- package/src/__tests__/cost-tracker.test.ts +274 -0
- package/src/__tests__/cron.test.ts +197 -0
- package/src/__tests__/dream.test.ts +204 -0
- package/src/__tests__/error-handler.test.ts +192 -0
- package/src/__tests__/features.test.ts +69 -0
- package/src/__tests__/file-history.test.ts +177 -0
- package/src/__tests__/hooks.test.ts +145 -0
- package/src/__tests__/keybindings.test.ts +159 -0
- package/src/__tests__/model-patches.test.ts +82 -0
- package/src/__tests__/permissions-rules.test.ts +121 -0
- package/src/__tests__/permissions.test.ts +108 -0
- package/src/__tests__/project-config.test.ts +63 -0
- package/src/__tests__/retry.test.ts +321 -0
- package/src/__tests__/router.test.ts +158 -0
- package/src/__tests__/session-compact.test.ts +191 -0
- package/src/__tests__/session.test.ts +145 -0
- package/src/__tests__/skill-registry.test.ts +130 -0
- package/src/__tests__/speculation.test.ts +196 -0
- package/src/__tests__/tasks-v2.test.ts +267 -0
- package/src/__tests__/telemetry.test.ts +149 -0
- package/src/__tests__/tool-executor.test.ts +141 -0
- package/src/__tests__/tool-registry.test.ts +166 -0
- package/src/__tests__/undercover.test.ts +93 -0
- package/src/__tests__/workflow.test.ts +195 -0
- package/src/agent/async-context.ts +64 -0
- package/src/agent/context.ts +245 -0
- package/src/agent/cron.ts +189 -0
- package/src/agent/dream.ts +165 -0
- package/src/agent/error-handler.ts +108 -0
- package/src/agent/ipc.ts +256 -0
- package/src/agent/kairos.ts +207 -0
- package/src/agent/loop.ts +314 -0
- package/src/agent/model-patches.ts +68 -0
- package/src/agent/speculation.ts +219 -0
- package/src/agent/sub-agent.ts +125 -0
- package/src/agent/system-prompt.ts +231 -0
- package/src/agent/team.ts +220 -0
- package/src/agent/tool-executor.ts +162 -0
- package/src/agent/workflow.ts +189 -0
- package/src/agent/worktree-manager.ts +86 -0
- package/src/autopilot/queue.ts +186 -0
- package/src/autopilot/scanner.ts +245 -0
- package/src/autopilot/types.ts +58 -0
- package/src/bridge/bridge-client.ts +57 -0
- package/src/bridge/bridge-server.ts +81 -0
- package/src/cli.ts +1120 -0
- package/src/config/features.ts +51 -0
- package/src/config/git.ts +137 -0
- package/src/config/hooks.ts +201 -0
- package/src/config/permissions.ts +251 -0
- package/src/config/project-config.ts +63 -0
- package/src/config/remote-settings.ts +163 -0
- package/src/config/settings-sync.ts +170 -0
- package/src/config/settings.ts +113 -0
- package/src/config/undercover.ts +76 -0
- package/src/config/upgrade-notice.ts +65 -0
- package/src/mcp/client.ts +197 -0
- package/src/mcp/manager.ts +125 -0
- package/src/mcp/oauth.ts +252 -0
- package/src/mcp/types.ts +61 -0
- package/src/persistence/memory.ts +129 -0
- package/src/persistence/session.ts +289 -0
- package/src/planning/plan-mode.ts +128 -0
- package/src/planning/plan-tools.ts +138 -0
- package/src/providers/anthropic.ts +177 -0
- package/src/providers/cost-tracker.ts +184 -0
- package/src/providers/retry.ts +264 -0
- package/src/providers/router.ts +159 -0
- package/src/providers/types.ts +79 -0
- package/src/providers/xai.ts +217 -0
- package/src/repl.tsx +1384 -0
- package/src/setup.ts +119 -0
- package/src/skills/loader.ts +78 -0
- package/src/skills/registry.ts +78 -0
- package/src/skills/types.ts +11 -0
- package/src/state/file-history.ts +264 -0
- package/src/telemetry/event-log.ts +116 -0
- package/src/tools/agent.ts +133 -0
- package/src/tools/ask-user.ts +229 -0
- package/src/tools/bash.ts +146 -0
- package/src/tools/config.ts +147 -0
- package/src/tools/diff.ts +137 -0
- package/src/tools/file-edit.ts +123 -0
- package/src/tools/file-read.ts +82 -0
- package/src/tools/file-write.ts +82 -0
- package/src/tools/glob.ts +76 -0
- package/src/tools/grep.ts +187 -0
- package/src/tools/ls.ts +77 -0
- package/src/tools/lsp.ts +375 -0
- package/src/tools/mcp-resources.ts +83 -0
- package/src/tools/mcp-tool.ts +47 -0
- package/src/tools/memory.ts +148 -0
- package/src/tools/notebook-edit.ts +133 -0
- package/src/tools/peers.ts +113 -0
- package/src/tools/powershell.ts +83 -0
- package/src/tools/registry.ts +114 -0
- package/src/tools/send-message.ts +75 -0
- package/src/tools/sleep.ts +50 -0
- package/src/tools/snip.ts +143 -0
- package/src/tools/tasks.ts +349 -0
- package/src/tools/team.ts +309 -0
- package/src/tools/todo-write.ts +93 -0
- package/src/tools/tool-search.ts +83 -0
- package/src/tools/types.ts +52 -0
- package/src/tools/web-browser.ts +263 -0
- package/src/tools/web-fetch.ts +118 -0
- package/src/tools/web-search.ts +107 -0
- package/src/tools/workflow.ts +188 -0
- package/src/tools/worktree.ts +143 -0
- package/src/types/branded.ts +22 -0
- package/src/ui/App.tsx +184 -0
- package/src/ui/BuddyPanel.tsx +52 -0
- package/src/ui/PermissionPrompt.tsx +29 -0
- package/src/ui/banner.ts +217 -0
- package/src/ui/buddy-ai.ts +108 -0
- package/src/ui/buddy.ts +466 -0
- package/src/ui/context-bar.ts +60 -0
- package/src/ui/effort.ts +65 -0
- package/src/ui/keybindings.ts +143 -0
- package/src/ui/markdown.ts +271 -0
- package/src/ui/message-renderer.ts +73 -0
- package/src/ui/mode.ts +80 -0
- package/src/ui/notifications.ts +57 -0
- package/src/ui/speech-bubble.ts +95 -0
- package/src/ui/spinner.ts +116 -0
- package/src/ui/theme.ts +98 -0
- package/src/version.ts +5 -0
- package/src/voice/voice-mode.ts +169 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote Managed Settings — poll a server for configuration overrides.
|
|
3
|
+
* Supports killswitches, model overrides, and feature flag updates.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync } from "fs";
|
|
7
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { getConfigDir } from "./settings.ts";
|
|
10
|
+
import { setFeature } from "./features.ts";
|
|
11
|
+
|
|
12
|
+
export interface RemoteSettings {
|
|
13
|
+
version: number;
|
|
14
|
+
features?: Record<string, boolean>;
|
|
15
|
+
modelOverride?: string;
|
|
16
|
+
effortOverride?: "low" | "normal" | "high";
|
|
17
|
+
killswitches?: {
|
|
18
|
+
bypassPermissions?: boolean;
|
|
19
|
+
voiceMode?: boolean;
|
|
20
|
+
kairosMode?: boolean;
|
|
21
|
+
teamMode?: boolean;
|
|
22
|
+
};
|
|
23
|
+
message?: string; // Display to user on next startup
|
|
24
|
+
fetchedAt: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const DEFAULT_POLL_INTERVAL = 60 * 60 * 1000; // 1 hour
|
|
28
|
+
const FETCH_TIMEOUT = 10_000; // 10s
|
|
29
|
+
|
|
30
|
+
let _remoteUrl: string | null = null;
|
|
31
|
+
let _apiKey: string | null = null;
|
|
32
|
+
let _pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
33
|
+
let _currentSettings: RemoteSettings | null = null;
|
|
34
|
+
let _onSettingsUpdate: ((settings: RemoteSettings) => void) | null = null;
|
|
35
|
+
|
|
36
|
+
function getCachePath(): string {
|
|
37
|
+
return join(getConfigDir(), "remote-settings.json");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Initialize remote settings with endpoint and API key */
|
|
41
|
+
export function initRemoteSettings(
|
|
42
|
+
url: string,
|
|
43
|
+
apiKey: string,
|
|
44
|
+
onUpdate?: (settings: RemoteSettings) => void,
|
|
45
|
+
): void {
|
|
46
|
+
_remoteUrl = url;
|
|
47
|
+
_apiKey = apiKey;
|
|
48
|
+
_onSettingsUpdate = onUpdate ?? null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Start polling for remote settings */
|
|
52
|
+
export function startPolling(
|
|
53
|
+
intervalMs: number = DEFAULT_POLL_INTERVAL,
|
|
54
|
+
): void {
|
|
55
|
+
if (_pollTimer || !_remoteUrl) return;
|
|
56
|
+
|
|
57
|
+
// Fetch immediately, then poll
|
|
58
|
+
fetchRemoteSettings().catch(() => {});
|
|
59
|
+
|
|
60
|
+
_pollTimer = setInterval(() => {
|
|
61
|
+
fetchRemoteSettings().catch(() => {});
|
|
62
|
+
}, intervalMs);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Stop polling */
|
|
66
|
+
export function stopPolling(): void {
|
|
67
|
+
if (_pollTimer) {
|
|
68
|
+
clearInterval(_pollTimer);
|
|
69
|
+
_pollTimer = null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Fetch remote settings from server */
|
|
74
|
+
async function fetchRemoteSettings(): Promise<void> {
|
|
75
|
+
if (!_remoteUrl || !_apiKey) return;
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const controller = new AbortController();
|
|
79
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
|
|
80
|
+
|
|
81
|
+
const response = await fetch(_remoteUrl, {
|
|
82
|
+
headers: {
|
|
83
|
+
Authorization: `Bearer ${_apiKey}`,
|
|
84
|
+
Accept: "application/json",
|
|
85
|
+
},
|
|
86
|
+
signal: controller.signal,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
clearTimeout(timeout);
|
|
90
|
+
|
|
91
|
+
if (!response.ok) return;
|
|
92
|
+
|
|
93
|
+
const data = (await response.json()) as Omit<RemoteSettings, "fetchedAt">;
|
|
94
|
+
const settings: RemoteSettings = {
|
|
95
|
+
...data,
|
|
96
|
+
fetchedAt: new Date().toISOString(),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
await applySettings(settings);
|
|
100
|
+
await cacheSettings(settings);
|
|
101
|
+
_currentSettings = settings;
|
|
102
|
+
_onSettingsUpdate?.(settings);
|
|
103
|
+
} catch {
|
|
104
|
+
// Never crash on remote settings failure — this is best-effort
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Apply remote settings to the running instance */
|
|
109
|
+
async function applySettings(settings: RemoteSettings): Promise<void> {
|
|
110
|
+
// Apply feature flags
|
|
111
|
+
if (settings.features) {
|
|
112
|
+
for (const [flag, enabled] of Object.entries(settings.features)) {
|
|
113
|
+
setFeature(flag, enabled);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Apply killswitches (these override local settings — false means killed)
|
|
118
|
+
if (settings.killswitches) {
|
|
119
|
+
if (settings.killswitches.voiceMode === false)
|
|
120
|
+
setFeature("VOICE_MODE", false);
|
|
121
|
+
if (settings.killswitches.kairosMode === false)
|
|
122
|
+
setFeature("KAIROS", false);
|
|
123
|
+
if (settings.killswitches.teamMode === false)
|
|
124
|
+
setFeature("TEAM_MODE", false);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Cache settings to disk for offline use */
|
|
129
|
+
async function cacheSettings(settings: RemoteSettings): Promise<void> {
|
|
130
|
+
await mkdir(getConfigDir(), { recursive: true });
|
|
131
|
+
await writeFile(
|
|
132
|
+
getCachePath(),
|
|
133
|
+
JSON.stringify(settings, null, 2),
|
|
134
|
+
"utf-8",
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Load cached settings (for offline startup) */
|
|
139
|
+
export async function loadCachedSettings(): Promise<RemoteSettings | null> {
|
|
140
|
+
const cachePath = getCachePath();
|
|
141
|
+
if (!existsSync(cachePath)) return null;
|
|
142
|
+
try {
|
|
143
|
+
const raw = await readFile(cachePath, "utf-8");
|
|
144
|
+
const settings = JSON.parse(raw) as RemoteSettings;
|
|
145
|
+
_currentSettings = settings;
|
|
146
|
+
await applySettings(settings);
|
|
147
|
+
return settings;
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Get current remote settings */
|
|
154
|
+
export function getRemoteSettings(): RemoteSettings | null {
|
|
155
|
+
return _currentSettings;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Check if a specific killswitch is active (i.e. the feature is killed) */
|
|
159
|
+
export function isKillswitchActive(
|
|
160
|
+
name: keyof NonNullable<RemoteSettings["killswitches"]>,
|
|
161
|
+
): boolean {
|
|
162
|
+
return _currentSettings?.killswitches?.[name] === false;
|
|
163
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings Sync — cross-device configuration synchronization.
|
|
3
|
+
*
|
|
4
|
+
* Two modes:
|
|
5
|
+
* 1. Export/Import — manual file-based sync
|
|
6
|
+
* 2. Git-based — auto-sync settings to a git repo
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync } from "fs";
|
|
10
|
+
import { readFile, writeFile, mkdir, readdir, copyFile, stat } from "fs/promises";
|
|
11
|
+
import { join, dirname } from "path";
|
|
12
|
+
import { hostname } from "os";
|
|
13
|
+
import { getConfigDir } from "./settings.ts";
|
|
14
|
+
|
|
15
|
+
interface SyncManifest {
|
|
16
|
+
version: number;
|
|
17
|
+
exportedAt: string;
|
|
18
|
+
hostname: string;
|
|
19
|
+
platform: string;
|
|
20
|
+
files: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const SYNCABLE_FILES = [
|
|
24
|
+
"settings.json",
|
|
25
|
+
"permissions.json",
|
|
26
|
+
"keybindings.json",
|
|
27
|
+
"buddy.json",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const SYNCABLE_DIRS = [
|
|
31
|
+
"memory",
|
|
32
|
+
"workflows",
|
|
33
|
+
"triggers",
|
|
34
|
+
"teams",
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Export settings to a sync bundle directory.
|
|
39
|
+
*/
|
|
40
|
+
export async function exportSettings(targetDir: string): Promise<SyncManifest> {
|
|
41
|
+
const configDir = getConfigDir();
|
|
42
|
+
await mkdir(targetDir, { recursive: true });
|
|
43
|
+
|
|
44
|
+
const files: string[] = [];
|
|
45
|
+
|
|
46
|
+
// Copy individual files
|
|
47
|
+
for (const file of SYNCABLE_FILES) {
|
|
48
|
+
const src = join(configDir, file);
|
|
49
|
+
if (existsSync(src)) {
|
|
50
|
+
await copyFile(src, join(targetDir, file));
|
|
51
|
+
files.push(file);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Copy directories
|
|
56
|
+
for (const dir of SYNCABLE_DIRS) {
|
|
57
|
+
const srcDir = join(configDir, dir);
|
|
58
|
+
if (!existsSync(srcDir)) continue;
|
|
59
|
+
|
|
60
|
+
const destDir = join(targetDir, dir);
|
|
61
|
+
await mkdir(destDir, { recursive: true });
|
|
62
|
+
|
|
63
|
+
const dirFiles = await readdir(srcDir);
|
|
64
|
+
for (const f of dirFiles) {
|
|
65
|
+
if (f.startsWith(".")) continue;
|
|
66
|
+
await copyFile(join(srcDir, f), join(destDir, f));
|
|
67
|
+
files.push(`${dir}/${f}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const manifest: SyncManifest = {
|
|
72
|
+
version: 1,
|
|
73
|
+
exportedAt: new Date().toISOString(),
|
|
74
|
+
hostname: hostname(),
|
|
75
|
+
platform: process.platform,
|
|
76
|
+
files,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
await writeFile(join(targetDir, "sync-manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
|
|
80
|
+
return manifest;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Import settings from a sync bundle directory.
|
|
85
|
+
*/
|
|
86
|
+
export async function importSettings(
|
|
87
|
+
sourceDir: string,
|
|
88
|
+
options: { overwrite?: boolean; merge?: boolean } = {},
|
|
89
|
+
): Promise<{ imported: string[]; skipped: string[] }> {
|
|
90
|
+
const configDir = getConfigDir();
|
|
91
|
+
const manifestPath = join(sourceDir, "sync-manifest.json");
|
|
92
|
+
|
|
93
|
+
if (!existsSync(manifestPath)) {
|
|
94
|
+
throw new Error("No sync-manifest.json found in source directory");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf-8")) as SyncManifest;
|
|
98
|
+
const imported: string[] = [];
|
|
99
|
+
const skipped: string[] = [];
|
|
100
|
+
|
|
101
|
+
for (const file of manifest.files) {
|
|
102
|
+
const src = join(sourceDir, file);
|
|
103
|
+
const dest = join(configDir, file);
|
|
104
|
+
|
|
105
|
+
if (!existsSync(src)) {
|
|
106
|
+
skipped.push(file);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Create parent directory if needed
|
|
111
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
112
|
+
|
|
113
|
+
if (existsSync(dest) && !options.overwrite) {
|
|
114
|
+
if (options.merge && file.endsWith(".json")) {
|
|
115
|
+
// Merge JSON files — incoming values override existing
|
|
116
|
+
try {
|
|
117
|
+
const existing = JSON.parse(await readFile(dest, "utf-8"));
|
|
118
|
+
const incoming = JSON.parse(await readFile(src, "utf-8"));
|
|
119
|
+
const merged = { ...existing, ...incoming };
|
|
120
|
+
await writeFile(dest, JSON.stringify(merged, null, 2), "utf-8");
|
|
121
|
+
imported.push(`${file} (merged)`);
|
|
122
|
+
continue;
|
|
123
|
+
} catch {
|
|
124
|
+
// Fall through to skip if merge fails
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
skipped.push(`${file} (exists)`);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
await copyFile(src, dest);
|
|
132
|
+
imported.push(file);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { imported, skipped };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get sync status — what would be synced.
|
|
140
|
+
*/
|
|
141
|
+
export async function getSyncStatus(): Promise<{ files: string[]; totalSize: number }> {
|
|
142
|
+
const configDir = getConfigDir();
|
|
143
|
+
const files: string[] = [];
|
|
144
|
+
let totalSize = 0;
|
|
145
|
+
|
|
146
|
+
for (const file of SYNCABLE_FILES) {
|
|
147
|
+
const path = join(configDir, file);
|
|
148
|
+
if (existsSync(path)) {
|
|
149
|
+
const s = await stat(path);
|
|
150
|
+
files.push(`${file} (${formatSize(s.size)})`);
|
|
151
|
+
totalSize += s.size;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const dir of SYNCABLE_DIRS) {
|
|
156
|
+
const dirPath = join(configDir, dir);
|
|
157
|
+
if (!existsSync(dirPath)) continue;
|
|
158
|
+
const dirFiles = await readdir(dirPath);
|
|
159
|
+
const count = dirFiles.filter((f) => !f.startsWith(".")).length;
|
|
160
|
+
if (count > 0) files.push(`${dir}/ (${count} files)`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return { files, totalSize };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function formatSize(bytes: number): string {
|
|
167
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
168
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
169
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
170
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings — configuration management for AshlrCode.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync } from "fs";
|
|
6
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
import type { ProviderRouterConfig } from "../providers/types.ts";
|
|
10
|
+
import type { HooksConfig } from "./hooks.ts";
|
|
11
|
+
import type { MCPServerConfig } from "../mcp/types.ts";
|
|
12
|
+
|
|
13
|
+
export interface ToolHookRule {
|
|
14
|
+
/** Glob pattern for tool name (e.g. "Bash", "File*") */
|
|
15
|
+
tool?: string;
|
|
16
|
+
/** Regex to match against JSON-serialized input */
|
|
17
|
+
inputPattern?: string;
|
|
18
|
+
/** Shell command to run (gets TOOL_NAME, TOOL_INPUT env vars) */
|
|
19
|
+
command?: string;
|
|
20
|
+
/** Direct action without running a command */
|
|
21
|
+
action?: "allow" | "deny";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface PostToolHookRule {
|
|
25
|
+
/** Glob pattern for tool name */
|
|
26
|
+
tool?: string;
|
|
27
|
+
/** Shell command to run (gets TOOL_NAME, TOOL_INPUT, TOOL_RESULT env vars) */
|
|
28
|
+
command?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Settings {
|
|
32
|
+
providers: ProviderRouterConfig;
|
|
33
|
+
defaultModel?: string;
|
|
34
|
+
maxTokens?: number;
|
|
35
|
+
hooks?: HooksConfig;
|
|
36
|
+
toolHooks?: {
|
|
37
|
+
preToolUse?: ToolHookRule[];
|
|
38
|
+
postToolUse?: PostToolHookRule[];
|
|
39
|
+
};
|
|
40
|
+
mcpServers?: Record<string, MCPServerConfig>;
|
|
41
|
+
permissionRules?: Array<{ tool: string; inputPattern?: string; action: "allow" | "deny" | "ask" }>;
|
|
42
|
+
remoteSettingsUrl?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let configDirOverride: string | null = null;
|
|
46
|
+
|
|
47
|
+
function getDefaultConfigDir(): string {
|
|
48
|
+
return process.env.ASHLRCODE_CONFIG_DIR ?? join(homedir(), ".ashlrcode");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getSettingsPath(): string {
|
|
52
|
+
return join(getConfigDir(), "settings.json");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function loadSettings(): Promise<Settings> {
|
|
56
|
+
const defaults = getDefaultSettings();
|
|
57
|
+
const settingsPath = getSettingsPath();
|
|
58
|
+
|
|
59
|
+
if (existsSync(settingsPath)) {
|
|
60
|
+
const raw = await readFile(settingsPath, "utf-8");
|
|
61
|
+
const fileSettings = JSON.parse(raw) as Partial<Settings>;
|
|
62
|
+
|
|
63
|
+
// Merge file settings with defaults — file settings override but
|
|
64
|
+
// providers always come from env vars / defaults if not in file
|
|
65
|
+
return {
|
|
66
|
+
...defaults,
|
|
67
|
+
...fileSettings,
|
|
68
|
+
providers: fileSettings.providers ?? defaults.providers,
|
|
69
|
+
hooks: fileSettings.hooks ?? defaults.hooks,
|
|
70
|
+
toolHooks: fileSettings.toolHooks ?? defaults.toolHooks,
|
|
71
|
+
mcpServers: fileSettings.mcpServers ?? defaults.mcpServers,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return defaults;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function saveSettings(settings: Settings): Promise<void> {
|
|
79
|
+
const configDir = getConfigDir();
|
|
80
|
+
await mkdir(configDir, { recursive: true });
|
|
81
|
+
await writeFile(getSettingsPath(), JSON.stringify(settings, null, 2), "utf-8");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getDefaultSettings(): Settings {
|
|
85
|
+
return {
|
|
86
|
+
providers: {
|
|
87
|
+
primary: {
|
|
88
|
+
provider: "xai",
|
|
89
|
+
apiKey: process.env.XAI_API_KEY ?? "",
|
|
90
|
+
model: process.env.AC_MODEL ?? "grok-4-1-fast-reasoning",
|
|
91
|
+
baseURL: "https://api.x.ai/v1",
|
|
92
|
+
},
|
|
93
|
+
fallbacks: process.env.ANTHROPIC_API_KEY
|
|
94
|
+
? [
|
|
95
|
+
{
|
|
96
|
+
provider: "anthropic",
|
|
97
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
98
|
+
model: "claude-sonnet-4-6-20250514",
|
|
99
|
+
},
|
|
100
|
+
]
|
|
101
|
+
: [],
|
|
102
|
+
},
|
|
103
|
+
maxTokens: 8192,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function getConfigDir(): string {
|
|
108
|
+
return configDirOverride ?? getDefaultConfigDir();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function setConfigDirForTests(configDir: string | null): void {
|
|
112
|
+
configDirOverride = configDir;
|
|
113
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Undercover Mode — masks AI attribution in output and commits.
|
|
3
|
+
*
|
|
4
|
+
* When active, the agent avoids revealing it's an AI:
|
|
5
|
+
* - No Co-Authored-By lines in commits
|
|
6
|
+
* - No model names or codenames in output
|
|
7
|
+
* - No "as an AI" language
|
|
8
|
+
* - No tool names or internal terminology
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
let _undercoverMode = false;
|
|
12
|
+
|
|
13
|
+
export function isUndercoverMode(): boolean {
|
|
14
|
+
return _undercoverMode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function setUndercoverMode(enabled: boolean): void {
|
|
18
|
+
_undercoverMode = enabled;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Model codenames to mask in output */
|
|
22
|
+
const CODENAMES = [
|
|
23
|
+
"capybara", "fennec", "numbat", "grok", "claude",
|
|
24
|
+
"opus", "sonnet", "haiku", "ashlrcode", "ashlr",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Mask sensitive codenames in text output.
|
|
29
|
+
* e.g., "capybara-v8" → "cap*****-v8"
|
|
30
|
+
*/
|
|
31
|
+
export function maskCodenames(text: string): string {
|
|
32
|
+
if (!_undercoverMode) return text;
|
|
33
|
+
|
|
34
|
+
let result = text;
|
|
35
|
+
for (const name of CODENAMES) {
|
|
36
|
+
const regex = new RegExp(`\\b${name}`, "gi");
|
|
37
|
+
result = result.replace(regex, (match) => {
|
|
38
|
+
if (match.length <= 3) return match;
|
|
39
|
+
return match.slice(0, 3) + "*".repeat(match.length - 3);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Strip AI attribution from commit messages.
|
|
47
|
+
*/
|
|
48
|
+
export function sanitizeCommitMessage(message: string): string {
|
|
49
|
+
if (!_undercoverMode) return message;
|
|
50
|
+
|
|
51
|
+
return message
|
|
52
|
+
.replace(/Co-Authored-By:.*$/gm, "")
|
|
53
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
54
|
+
.trim();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get undercover system prompt addition.
|
|
59
|
+
*/
|
|
60
|
+
export function getUndercoverPrompt(): string {
|
|
61
|
+
if (!_undercoverMode) return "";
|
|
62
|
+
|
|
63
|
+
return `
|
|
64
|
+
|
|
65
|
+
## UNDERCOVER MODE ACTIVE
|
|
66
|
+
|
|
67
|
+
You are operating in undercover mode. Follow these rules strictly:
|
|
68
|
+
- Do NOT reveal that you are an AI, language model, or automated tool
|
|
69
|
+
- Do NOT mention model names (Claude, GPT, Grok, etc.)
|
|
70
|
+
- Do NOT add Co-Authored-By lines to commits
|
|
71
|
+
- Do NOT reference internal tooling, codenames, or model versions
|
|
72
|
+
- Write code and messages as if you are a human developer
|
|
73
|
+
- If asked about your identity, deflect naturally
|
|
74
|
+
- Do NOT use phrases like "as an AI", "I'm a language model", etc.
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upgrade notice — check for new releases on startup.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
6
|
+
import { existsSync } from "fs";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
import { getConfigDir } from "./settings.ts";
|
|
9
|
+
|
|
10
|
+
const CHECK_INTERVAL = 24 * 60 * 60 * 1000; // Once per day
|
|
11
|
+
|
|
12
|
+
interface UpgradeState {
|
|
13
|
+
lastCheck: string;
|
|
14
|
+
latestVersion?: string;
|
|
15
|
+
currentVersion: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getStatePath(): string {
|
|
19
|
+
return join(getConfigDir(), "upgrade-state.json");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function checkForUpgrade(currentVersion: string): Promise<string | null> {
|
|
23
|
+
const path = getStatePath();
|
|
24
|
+
|
|
25
|
+
// Check if we already checked recently
|
|
26
|
+
if (existsSync(path)) {
|
|
27
|
+
try {
|
|
28
|
+
const raw = await readFile(path, "utf-8");
|
|
29
|
+
const state = JSON.parse(raw) as UpgradeState;
|
|
30
|
+
if (Date.now() - new Date(state.lastCheck).getTime() < CHECK_INTERVAL) {
|
|
31
|
+
// Already checked recently — return cached result
|
|
32
|
+
if (state.latestVersion && state.latestVersion !== currentVersion) {
|
|
33
|
+
return state.latestVersion;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
} catch {}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
// Check npm registry for latest version
|
|
42
|
+
const controller = new AbortController();
|
|
43
|
+
setTimeout(() => controller.abort(), 5000);
|
|
44
|
+
const response = await fetch("https://registry.npmjs.org/ashlrcode/latest", {
|
|
45
|
+
signal: controller.signal,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (!response.ok) return null;
|
|
49
|
+
const data = await response.json() as { version: string };
|
|
50
|
+
|
|
51
|
+
const state: UpgradeState = {
|
|
52
|
+
lastCheck: new Date().toISOString(),
|
|
53
|
+
latestVersion: data.version,
|
|
54
|
+
currentVersion,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
await mkdir(getConfigDir(), { recursive: true });
|
|
58
|
+
await writeFile(getStatePath(), JSON.stringify(state), "utf-8");
|
|
59
|
+
|
|
60
|
+
if (data.version !== currentVersion) return data.version;
|
|
61
|
+
return null;
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|