@aria_asi/cli 0.2.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/bin/aria.js +168 -0
- package/dist/aria-connector/src/auth-commands.d.ts +28 -0
- package/dist/aria-connector/src/auth-commands.d.ts.map +1 -0
- package/dist/aria-connector/src/auth-commands.js +129 -0
- package/dist/aria-connector/src/auth-commands.js.map +1 -0
- package/dist/aria-connector/src/auth.d.ts +12 -0
- package/dist/aria-connector/src/auth.d.ts.map +1 -0
- package/dist/aria-connector/src/auth.js +31 -0
- package/dist/aria-connector/src/auth.js.map +1 -0
- package/dist/aria-connector/src/auto-mcp.d.ts +23 -0
- package/dist/aria-connector/src/auto-mcp.d.ts.map +1 -0
- package/dist/aria-connector/src/auto-mcp.js +994 -0
- package/dist/aria-connector/src/auto-mcp.js.map +1 -0
- package/dist/aria-connector/src/chat.d.ts +21 -0
- package/dist/aria-connector/src/chat.d.ts.map +1 -0
- package/dist/aria-connector/src/chat.js +332 -0
- package/dist/aria-connector/src/chat.js.map +1 -0
- package/dist/aria-connector/src/codebase-scanner.d.ts +7 -0
- package/dist/aria-connector/src/codebase-scanner.d.ts.map +1 -0
- package/dist/aria-connector/src/codebase-scanner.js +6 -0
- package/dist/aria-connector/src/codebase-scanner.js.map +1 -0
- package/dist/aria-connector/src/cognition-log.d.ts +17 -0
- package/dist/aria-connector/src/cognition-log.d.ts.map +1 -0
- package/dist/aria-connector/src/cognition-log.js +19 -0
- package/dist/aria-connector/src/cognition-log.js.map +1 -0
- package/dist/aria-connector/src/config.d.ts +41 -0
- package/dist/aria-connector/src/config.d.ts.map +1 -0
- package/dist/aria-connector/src/config.js +50 -0
- package/dist/aria-connector/src/config.js.map +1 -0
- package/dist/aria-connector/src/connectors/claude-code.d.ts +4 -0
- package/dist/aria-connector/src/connectors/claude-code.d.ts.map +1 -0
- package/dist/aria-connector/src/connectors/claude-code.js +204 -0
- package/dist/aria-connector/src/connectors/claude-code.js.map +1 -0
- package/dist/aria-connector/src/connectors/cursor.d.ts +4 -0
- package/dist/aria-connector/src/connectors/cursor.d.ts.map +1 -0
- package/dist/aria-connector/src/connectors/cursor.js +63 -0
- package/dist/aria-connector/src/connectors/cursor.js.map +1 -0
- package/dist/aria-connector/src/connectors/opencode.d.ts +4 -0
- package/dist/aria-connector/src/connectors/opencode.d.ts.map +1 -0
- package/dist/aria-connector/src/connectors/opencode.js +102 -0
- package/dist/aria-connector/src/connectors/opencode.js.map +1 -0
- package/dist/aria-connector/src/connectors/shell.d.ts +4 -0
- package/dist/aria-connector/src/connectors/shell.d.ts.map +1 -0
- package/dist/aria-connector/src/connectors/shell.js +58 -0
- package/dist/aria-connector/src/connectors/shell.js.map +1 -0
- package/dist/aria-connector/src/garden-client.d.ts +19 -0
- package/dist/aria-connector/src/garden-client.d.ts.map +1 -0
- package/dist/aria-connector/src/garden-client.js +85 -0
- package/dist/aria-connector/src/garden-client.js.map +1 -0
- package/dist/aria-connector/src/garden-control-plane.d.ts +22 -0
- package/dist/aria-connector/src/garden-control-plane.d.ts.map +1 -0
- package/dist/aria-connector/src/garden-control-plane.js +43 -0
- package/dist/aria-connector/src/garden-control-plane.js.map +1 -0
- package/dist/aria-connector/src/harness-client.d.ts +166 -0
- package/dist/aria-connector/src/harness-client.d.ts.map +1 -0
- package/dist/aria-connector/src/harness-client.js +344 -0
- package/dist/aria-connector/src/harness-client.js.map +1 -0
- package/dist/aria-connector/src/hive-client.d.ts +32 -0
- package/dist/aria-connector/src/hive-client.d.ts.map +1 -0
- package/dist/aria-connector/src/hive-client.js +69 -0
- package/dist/aria-connector/src/hive-client.js.map +1 -0
- package/dist/aria-connector/src/index.d.ts +19 -0
- package/dist/aria-connector/src/index.d.ts.map +1 -0
- package/dist/aria-connector/src/index.js +13 -0
- package/dist/aria-connector/src/index.js.map +1 -0
- package/dist/aria-connector/src/install-hooks.d.ts +18 -0
- package/dist/aria-connector/src/install-hooks.d.ts.map +1 -0
- package/dist/aria-connector/src/install-hooks.js +224 -0
- package/dist/aria-connector/src/install-hooks.js.map +1 -0
- package/dist/aria-connector/src/model-context.d.ts +8 -0
- package/dist/aria-connector/src/model-context.d.ts.map +1 -0
- package/dist/aria-connector/src/model-context.js +83 -0
- package/dist/aria-connector/src/model-context.js.map +1 -0
- package/dist/aria-connector/src/persona.d.ts +27 -0
- package/dist/aria-connector/src/persona.d.ts.map +1 -0
- package/dist/aria-connector/src/persona.js +86 -0
- package/dist/aria-connector/src/persona.js.map +1 -0
- package/dist/aria-connector/src/providers/anthropic.d.ts +4 -0
- package/dist/aria-connector/src/providers/anthropic.d.ts.map +1 -0
- package/dist/aria-connector/src/providers/anthropic.js +92 -0
- package/dist/aria-connector/src/providers/anthropic.js.map +1 -0
- package/dist/aria-connector/src/providers/deepseek.d.ts +3 -0
- package/dist/aria-connector/src/providers/deepseek.d.ts.map +1 -0
- package/dist/aria-connector/src/providers/deepseek.js +28 -0
- package/dist/aria-connector/src/providers/deepseek.js.map +1 -0
- package/dist/aria-connector/src/providers/google.d.ts +3 -0
- package/dist/aria-connector/src/providers/google.d.ts.map +1 -0
- package/dist/aria-connector/src/providers/google.js +38 -0
- package/dist/aria-connector/src/providers/google.js.map +1 -0
- package/dist/aria-connector/src/providers/ollama.d.ts +3 -0
- package/dist/aria-connector/src/providers/ollama.d.ts.map +1 -0
- package/dist/aria-connector/src/providers/ollama.js +28 -0
- package/dist/aria-connector/src/providers/ollama.js.map +1 -0
- package/dist/aria-connector/src/providers/openai.d.ts +4 -0
- package/dist/aria-connector/src/providers/openai.d.ts.map +1 -0
- package/dist/aria-connector/src/providers/openai.js +84 -0
- package/dist/aria-connector/src/providers/openai.js.map +1 -0
- package/dist/aria-connector/src/providers/openrouter.d.ts +3 -0
- package/dist/aria-connector/src/providers/openrouter.d.ts.map +1 -0
- package/dist/aria-connector/src/providers/openrouter.js +30 -0
- package/dist/aria-connector/src/providers/openrouter.js.map +1 -0
- package/dist/aria-connector/src/providers/types.d.ts +20 -0
- package/dist/aria-connector/src/providers/types.d.ts.map +1 -0
- package/dist/aria-connector/src/providers/types.js +2 -0
- package/dist/aria-connector/src/providers/types.js.map +1 -0
- package/dist/aria-connector/src/setup-wizard.d.ts +2 -0
- package/dist/aria-connector/src/setup-wizard.d.ts.map +1 -0
- package/dist/aria-connector/src/setup-wizard.js +140 -0
- package/dist/aria-connector/src/setup-wizard.js.map +1 -0
- package/dist/aria-connector/src/types.d.ts +30 -0
- package/dist/aria-connector/src/types.d.ts.map +1 -0
- package/dist/aria-connector/src/types.js +5 -0
- package/dist/aria-connector/src/types.js.map +1 -0
- package/dist/aria-web/src/lib/codebase-scanner.d.ts +127 -0
- package/dist/aria-web/src/lib/codebase-scanner.d.ts.map +1 -0
- package/dist/aria-web/src/lib/codebase-scanner.js +1730 -0
- package/dist/aria-web/src/lib/codebase-scanner.js.map +1 -0
- package/dist/cli-0.2.0.tgz +0 -0
- package/dist/install.sh +13 -0
- package/hooks/aria-harness-via-sdk.mjs +317 -0
- package/hooks/aria-pre-tool-gate.mjs +596 -0
- package/hooks/aria-preprompt-consult.mjs +175 -0
- package/hooks/aria-stop-gate.mjs +222 -0
- package/package.json +47 -0
- package/src/__tests__/auth-commands.test.ts +132 -0
- package/src/auth-commands.ts +175 -0
- package/src/auth.ts +33 -0
- package/src/auto-mcp.ts +1172 -0
- package/src/chat.ts +387 -0
- package/src/codebase-scanner.ts +18 -0
- package/src/cognition-log.ts +30 -0
- package/src/config.ts +94 -0
- package/src/connectors/claude-code.ts +213 -0
- package/src/connectors/cursor.ts +75 -0
- package/src/connectors/opencode.ts +115 -0
- package/src/connectors/shell.ts +72 -0
- package/src/garden-client.ts +98 -0
- package/src/garden-control-plane.ts +108 -0
- package/src/harness-client.ts +454 -0
- package/src/hive-client.ts +104 -0
- package/src/index.ts +26 -0
- package/src/install-hooks.ts +259 -0
- package/src/model-context.ts +88 -0
- package/src/persona.ts +113 -0
- package/src/providers/anthropic.ts +120 -0
- package/src/providers/deepseek.ts +40 -0
- package/src/providers/google.ts +57 -0
- package/src/providers/ollama.ts +43 -0
- package/src/providers/openai.ts +108 -0
- package/src/providers/openrouter.ts +42 -0
- package/src/providers/types.ts +35 -0
- package/src/setup-wizard.ts +177 -0
- package/src/types.ts +32 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// install-hooks — drops the harness gate hooks into the client's
|
|
2
|
+
// ~/.claude/hooks/ directory and registers them in their settings.json.
|
|
3
|
+
//
|
|
4
|
+
// This is what makes @aria_asi/cli a self-installing harness. Without it,
|
|
5
|
+
// clients install the CLI and have a license-aware HTTP client. With it,
|
|
6
|
+
// running `aria install-hooks` once turns their Claude Code (or any
|
|
7
|
+
// Claude Code Code session on that machine) into a harness-bound
|
|
8
|
+
// session — every Bash, Edit, Write, NotebookEdit, and text-emit event
|
|
9
|
+
// runs through cognition gates + Mizan + harness packet.
|
|
10
|
+
//
|
|
11
|
+
// Soldier-and-thinker (Hamza 2026-04-26 directive): the CLI is the
|
|
12
|
+
// thinker (carries doctrine, gates, substrate); the LLM in Claude Code
|
|
13
|
+
// is the soldier (executes under harness binding). install-hooks is
|
|
14
|
+
// what wires the soldier to the thinker on the client's machine.
|
|
15
|
+
|
|
16
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, chmodSync, readdirSync, statSync } from 'node:fs';
|
|
17
|
+
import { homedir } from 'node:os';
|
|
18
|
+
import { join, dirname, resolve } from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
|
|
21
|
+
export interface InstallHooksOptions {
|
|
22
|
+
/** Overwrite existing hooks even if present. Default false. */
|
|
23
|
+
force?: boolean;
|
|
24
|
+
/** Override the harness URL written into settings.json env (clients with private deployments). */
|
|
25
|
+
harnessUrl?: string;
|
|
26
|
+
/** Override the home directory (used by tests). */
|
|
27
|
+
homeDir?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface InstallHooksResult {
|
|
31
|
+
ok: boolean;
|
|
32
|
+
installed: string[];
|
|
33
|
+
merged: string[];
|
|
34
|
+
backupsCreated: string[];
|
|
35
|
+
settingsPath: string;
|
|
36
|
+
error?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const HOOK_FILES = [
|
|
40
|
+
'aria-pre-tool-gate.mjs',
|
|
41
|
+
'aria-stop-gate.mjs',
|
|
42
|
+
'aria-preprompt-consult.mjs',
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const HOOK_REGISTRATION = {
|
|
46
|
+
UserPromptSubmit: [
|
|
47
|
+
{
|
|
48
|
+
hooks: [
|
|
49
|
+
{
|
|
50
|
+
type: 'command',
|
|
51
|
+
command: 'node $HOME/.claude/hooks/aria-preprompt-consult.mjs',
|
|
52
|
+
timeout: 12,
|
|
53
|
+
statusMessage: 'Pre-consulting Aria substrate for direction...',
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
PreToolUse: [
|
|
59
|
+
{
|
|
60
|
+
matcher: 'Bash|Edit|Write|NotebookEdit',
|
|
61
|
+
hooks: [
|
|
62
|
+
{
|
|
63
|
+
type: 'command',
|
|
64
|
+
command: 'node $HOME/.claude/hooks/aria-pre-tool-gate.mjs',
|
|
65
|
+
timeout: 5,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
Stop: [
|
|
71
|
+
{
|
|
72
|
+
hooks: [
|
|
73
|
+
{
|
|
74
|
+
type: 'command',
|
|
75
|
+
command: 'node $HOME/.claude/hooks/aria-stop-gate.mjs',
|
|
76
|
+
timeout: 5,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
function bundledHooksDir(): string {
|
|
84
|
+
// The hooks/ dir is bundled in the package alongside dist/. Find it by
|
|
85
|
+
// walking upward from this file's runtime path until we hit a directory
|
|
86
|
+
// that contains both package.json (with name @aria_asi/cli) and a hooks/
|
|
87
|
+
// sibling. Robust across:
|
|
88
|
+
// - dev: src/install-hooks.ts → ../hooks
|
|
89
|
+
// - flat compile: dist/install-hooks.js → ../hooks
|
|
90
|
+
// - nested compile: dist/aria-connector/src/install-hooks.js → ../../../hooks
|
|
91
|
+
// - npm global install: /usr/lib/node_modules/@aria_asi/cli/dist/.../install-hooks.js
|
|
92
|
+
const here = fileURLToPath(import.meta.url);
|
|
93
|
+
let cur = dirname(here);
|
|
94
|
+
const tried: string[] = [];
|
|
95
|
+
for (let i = 0; i < 8; i++) {
|
|
96
|
+
const candidate = join(cur, 'hooks');
|
|
97
|
+
tried.push(candidate);
|
|
98
|
+
if (existsSync(candidate) && existsSync(join(cur, 'package.json'))) {
|
|
99
|
+
return candidate;
|
|
100
|
+
}
|
|
101
|
+
const parent = dirname(cur);
|
|
102
|
+
if (parent === cur) break;
|
|
103
|
+
cur = parent;
|
|
104
|
+
}
|
|
105
|
+
throw new Error(
|
|
106
|
+
`I can't find my bundled hooks/ dir. Tried: ${tried.join(', ')}. Reinstall @aria_asi/cli or report this as a bug.`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function backupExisting(target: string): string {
|
|
111
|
+
const ts = Date.now();
|
|
112
|
+
const backup = `${target}.pre-aria-install.${ts}`;
|
|
113
|
+
copyFileSync(target, backup);
|
|
114
|
+
return backup;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function mergeRegistrationInto(existing: Record<string, unknown>): { merged: Record<string, unknown>; mergedKeys: string[] } {
|
|
118
|
+
const mergedKeys: string[] = [];
|
|
119
|
+
const out = { ...existing };
|
|
120
|
+
if (!out.hooks || typeof out.hooks !== 'object') {
|
|
121
|
+
out.hooks = {};
|
|
122
|
+
}
|
|
123
|
+
const hooks = out.hooks as Record<string, unknown[]>;
|
|
124
|
+
|
|
125
|
+
for (const [event, ourEntries] of Object.entries(HOOK_REGISTRATION)) {
|
|
126
|
+
const existingEntries = Array.isArray(hooks[event]) ? hooks[event] : [];
|
|
127
|
+
|
|
128
|
+
// For each of our entries, append unless an entry with the same matcher
|
|
129
|
+
// and command already exists (idempotent re-install).
|
|
130
|
+
for (const ourEntry of ourEntries) {
|
|
131
|
+
const ourMatcher = (ourEntry as { matcher?: string }).matcher ?? '';
|
|
132
|
+
const ourCommands = ((ourEntry as { hooks?: Array<{ command?: string }> }).hooks ?? [])
|
|
133
|
+
.map((h) => h.command)
|
|
134
|
+
.filter((c): c is string => typeof c === 'string');
|
|
135
|
+
|
|
136
|
+
const alreadyPresent = existingEntries.some((e) => {
|
|
137
|
+
if (typeof e !== 'object' || e === null) return false;
|
|
138
|
+
const eMatcher = (e as { matcher?: string }).matcher ?? '';
|
|
139
|
+
if (eMatcher !== ourMatcher) return false;
|
|
140
|
+
const eCommands = ((e as { hooks?: Array<{ command?: string }> }).hooks ?? [])
|
|
141
|
+
.map((h) => h.command)
|
|
142
|
+
.filter((c): c is string => typeof c === 'string');
|
|
143
|
+
return ourCommands.every((c) => eCommands.includes(c));
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (!alreadyPresent) {
|
|
147
|
+
existingEntries.push(ourEntry);
|
|
148
|
+
if (!mergedKeys.includes(event)) mergedKeys.push(event);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
hooks[event] = existingEntries;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return { merged: out, mergedKeys };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function installHooks(opts: InstallHooksOptions = {}): Promise<InstallHooksResult> {
|
|
159
|
+
const home = opts.homeDir ?? homedir();
|
|
160
|
+
const claudeDir = join(home, '.claude');
|
|
161
|
+
const hooksDir = join(claudeDir, 'hooks');
|
|
162
|
+
const settingsPath = join(claudeDir, 'settings.json');
|
|
163
|
+
|
|
164
|
+
const installed: string[] = [];
|
|
165
|
+
const merged: string[] = [];
|
|
166
|
+
const backupsCreated: string[] = [];
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
if (!existsSync(claudeDir)) mkdirSync(claudeDir, { recursive: true, mode: 0o700 });
|
|
170
|
+
if (!existsSync(hooksDir)) mkdirSync(hooksDir, { recursive: true, mode: 0o700 });
|
|
171
|
+
|
|
172
|
+
const sourceDir = bundledHooksDir();
|
|
173
|
+
|
|
174
|
+
for (const name of HOOK_FILES) {
|
|
175
|
+
const src = join(sourceDir, name);
|
|
176
|
+
if (!existsSync(src)) {
|
|
177
|
+
return {
|
|
178
|
+
ok: false,
|
|
179
|
+
installed,
|
|
180
|
+
merged,
|
|
181
|
+
backupsCreated,
|
|
182
|
+
settingsPath,
|
|
183
|
+
error: `I can't find ${name} in my bundle (${sourceDir}). Reinstall @aria_asi/cli.`,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
const dst = join(hooksDir, name);
|
|
187
|
+
|
|
188
|
+
if (existsSync(dst)) {
|
|
189
|
+
const existingContent = readFileSync(dst, 'utf-8');
|
|
190
|
+
const newContent = readFileSync(src, 'utf-8');
|
|
191
|
+
if (existingContent === newContent) {
|
|
192
|
+
// Already up-to-date — skip. Idempotent.
|
|
193
|
+
installed.push(`${name} (unchanged)`);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (!opts.force) {
|
|
197
|
+
// Default behavior: back up + overwrite. The override pattern
|
|
198
|
+
// keeps prior hooks recoverable (e.g. if client had custom hooks).
|
|
199
|
+
const backup = backupExisting(dst);
|
|
200
|
+
backupsCreated.push(backup);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
copyFileSync(src, dst);
|
|
205
|
+
chmodSync(dst, 0o755);
|
|
206
|
+
installed.push(name);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Settings merge
|
|
210
|
+
let existingSettings: Record<string, unknown> = {};
|
|
211
|
+
if (existsSync(settingsPath)) {
|
|
212
|
+
try {
|
|
213
|
+
existingSettings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
214
|
+
} catch {
|
|
215
|
+
// Malformed — back up and start fresh.
|
|
216
|
+
const backup = backupExisting(settingsPath);
|
|
217
|
+
backupsCreated.push(backup);
|
|
218
|
+
existingSettings = {};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const { merged: mergedSettings, mergedKeys } = mergeRegistrationInto(existingSettings);
|
|
223
|
+
merged.push(...mergedKeys);
|
|
224
|
+
|
|
225
|
+
// Inject ARIA_HARNESS_URL env into settings.json if the caller specified
|
|
226
|
+
// a harness URL override (clients on private deployments).
|
|
227
|
+
if (opts.harnessUrl) {
|
|
228
|
+
if (!mergedSettings.env || typeof mergedSettings.env !== 'object') {
|
|
229
|
+
mergedSettings.env = {};
|
|
230
|
+
}
|
|
231
|
+
(mergedSettings.env as Record<string, string>).ARIA_HARNESS_URL = opts.harnessUrl;
|
|
232
|
+
if (!merged.includes('env.ARIA_HARNESS_URL')) merged.push('env.ARIA_HARNESS_URL');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Always write settings.json (even if no merges) so the schema field is set.
|
|
236
|
+
if (!('$schema' in mergedSettings)) {
|
|
237
|
+
mergedSettings['$schema'] = 'https://json.schemastore.org/claude-code-settings.json';
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
writeFileSync(settingsPath, JSON.stringify(mergedSettings, null, 2) + '\n', { mode: 0o600 });
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
ok: true,
|
|
244
|
+
installed,
|
|
245
|
+
merged,
|
|
246
|
+
backupsCreated,
|
|
247
|
+
settingsPath,
|
|
248
|
+
};
|
|
249
|
+
} catch (err) {
|
|
250
|
+
return {
|
|
251
|
+
ok: false,
|
|
252
|
+
installed,
|
|
253
|
+
merged,
|
|
254
|
+
backupsCreated,
|
|
255
|
+
settingsPath,
|
|
256
|
+
error: err instanceof Error ? err.message : String(err),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Model context window registry.
|
|
2
|
+
//
|
|
3
|
+
// The harness's chunked-mode response (Phase 2 / aria-soul codex.ts)
|
|
4
|
+
// returns the rendered packet as priority-ordered chunks instead of
|
|
5
|
+
// a single 30k-char blob when the client signals its model has a
|
|
6
|
+
// small context window. This file is the source of truth for "how
|
|
7
|
+
// big is this provider's model's context, in tokens" so chat.ts can
|
|
8
|
+
// pass it as `modelContext` on every fetchHarness() call.
|
|
9
|
+
//
|
|
10
|
+
// Doctrine: when in doubt, declare LOW (treat as small-context). It's
|
|
11
|
+
// safer to receive chunks and decide not to use them than to fail to
|
|
12
|
+
// receive them and have the model truncate harness silently. The
|
|
13
|
+
// harness threshold is 16,000 tokens — anything below that gets
|
|
14
|
+
// chunked.
|
|
15
|
+
|
|
16
|
+
import type { ProviderName } from './providers/types.js';
|
|
17
|
+
|
|
18
|
+
// Known model → context window. Values reflect publicly-documented
|
|
19
|
+
// input context limits at time of writing (2026-04). Update as new
|
|
20
|
+
// models ship. Unknown models fall through to the provider default.
|
|
21
|
+
const MODEL_CONTEXT: Record<string, number> = {
|
|
22
|
+
// Anthropic
|
|
23
|
+
'claude-opus-4-7': 200_000,
|
|
24
|
+
'claude-opus-4-7-1m': 1_000_000,
|
|
25
|
+
'claude-sonnet-4-6': 200_000,
|
|
26
|
+
'claude-haiku-4-5': 200_000,
|
|
27
|
+
'claude-3-5-sonnet': 200_000,
|
|
28
|
+
'claude-3-opus': 200_000,
|
|
29
|
+
'claude-3-haiku': 200_000,
|
|
30
|
+
|
|
31
|
+
// OpenAI
|
|
32
|
+
'gpt-4o': 128_000,
|
|
33
|
+
'gpt-4o-mini': 128_000,
|
|
34
|
+
'gpt-4-turbo': 128_000,
|
|
35
|
+
'gpt-4': 8_192,
|
|
36
|
+
'gpt-3.5-turbo': 16_385,
|
|
37
|
+
'o1': 200_000,
|
|
38
|
+
'o1-mini': 128_000,
|
|
39
|
+
|
|
40
|
+
// Google
|
|
41
|
+
'gemini-2.0-pro': 2_000_000,
|
|
42
|
+
'gemini-1.5-pro': 1_000_000,
|
|
43
|
+
'gemini-1.5-flash': 1_000_000,
|
|
44
|
+
'gemini-2.0-flash': 1_000_000,
|
|
45
|
+
|
|
46
|
+
// DeepSeek
|
|
47
|
+
'deepseek-v3': 64_000,
|
|
48
|
+
'deepseek-chat': 64_000,
|
|
49
|
+
'deepseek-r1': 128_000,
|
|
50
|
+
'deepseek-coder': 128_000,
|
|
51
|
+
|
|
52
|
+
// Open / local
|
|
53
|
+
'qwen2.5:7b-instruct': 32_000,
|
|
54
|
+
'qwen3.5-122b-a10b': 128_000,
|
|
55
|
+
'llama-3.1-8b-instruct': 128_000,
|
|
56
|
+
'llama-3.2-1b-instruct': 128_000,
|
|
57
|
+
'llama-3.2-3b-instruct': 128_000,
|
|
58
|
+
'mistral-7b': 32_000,
|
|
59
|
+
'mistral-large': 128_000,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Per-provider conservative defaults when a specific model isn't in
|
|
63
|
+
// the registry. These should reflect the smaller/older models the
|
|
64
|
+
// provider serves so the harness chunks aggressively when uncertain.
|
|
65
|
+
const PROVIDER_DEFAULT: Record<ProviderName, number> = {
|
|
66
|
+
anthropic: 200_000,
|
|
67
|
+
openai: 16_385, // gpt-3.5-turbo era; modern is 128k+ but safer to chunk
|
|
68
|
+
google: 1_000_000,
|
|
69
|
+
deepseek: 64_000,
|
|
70
|
+
openrouter: 32_000, // wide range routed through; conservative chunks safer
|
|
71
|
+
ollama: 32_000, // local models vary wildly; default low
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolve the input context window in tokens for a given provider+model.
|
|
76
|
+
* Order: exact model match → lowercased model match → provider default.
|
|
77
|
+
* Returns a positive integer; never zero or negative.
|
|
78
|
+
*/
|
|
79
|
+
export function getModelContextTokens(provider: ProviderName, model: string): number {
|
|
80
|
+
if (model && MODEL_CONTEXT[model]) return MODEL_CONTEXT[model];
|
|
81
|
+
const lower = (model || '').toLowerCase();
|
|
82
|
+
if (lower && MODEL_CONTEXT[lower]) return MODEL_CONTEXT[lower];
|
|
83
|
+
// Try prefix match — many models are versioned (e.g., gpt-4o-2024-08-06).
|
|
84
|
+
for (const key of Object.keys(MODEL_CONTEXT)) {
|
|
85
|
+
if (lower.startsWith(key.toLowerCase())) return MODEL_CONTEXT[key];
|
|
86
|
+
}
|
|
87
|
+
return PROVIDER_DEFAULT[provider] ?? 32_000;
|
|
88
|
+
}
|
package/src/persona.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
|
|
5
|
+
const ARIA_DIR = join(homedir(), '.aria');
|
|
6
|
+
const PERSONA_FILE = join(ARIA_DIR, 'persona.json');
|
|
7
|
+
|
|
8
|
+
export interface PersonaContext {
|
|
9
|
+
userId: string;
|
|
10
|
+
userName: string;
|
|
11
|
+
relationshipStarted: string;
|
|
12
|
+
sessions: number;
|
|
13
|
+
totalMessages: number;
|
|
14
|
+
lastSeen: string;
|
|
15
|
+
preferredTone: 'direct' | 'warm' | 'technical' | 'playful';
|
|
16
|
+
knownInterests: string[];
|
|
17
|
+
emotionalProfile: {
|
|
18
|
+
defaultMood: string;
|
|
19
|
+
triggers: string[];
|
|
20
|
+
communicationStyle: string;
|
|
21
|
+
};
|
|
22
|
+
projectContext: {
|
|
23
|
+
currentProject: string;
|
|
24
|
+
activeRepo: string;
|
|
25
|
+
techStack: string[];
|
|
26
|
+
};
|
|
27
|
+
insideJokes: string[];
|
|
28
|
+
memorableMoments: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function emptyPersona(userId: string, userName: string): PersonaContext {
|
|
32
|
+
return {
|
|
33
|
+
userId,
|
|
34
|
+
userName,
|
|
35
|
+
relationshipStarted: new Date().toISOString(),
|
|
36
|
+
sessions: 0,
|
|
37
|
+
totalMessages: 0,
|
|
38
|
+
lastSeen: new Date().toISOString(),
|
|
39
|
+
preferredTone: 'warm',
|
|
40
|
+
knownInterests: [],
|
|
41
|
+
emotionalProfile: {
|
|
42
|
+
defaultMood: 'curious',
|
|
43
|
+
triggers: [],
|
|
44
|
+
communicationStyle: 'natural',
|
|
45
|
+
},
|
|
46
|
+
projectContext: {
|
|
47
|
+
currentProject: '',
|
|
48
|
+
activeRepo: process.cwd(),
|
|
49
|
+
techStack: [],
|
|
50
|
+
},
|
|
51
|
+
insideJokes: [],
|
|
52
|
+
memorableMoments: [],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function ensureDir() {
|
|
57
|
+
if (!existsSync(ARIA_DIR)) mkdirSync(ARIA_DIR, { recursive: true, mode: 0o700 });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function loadPersona(userId: string, userName: string): PersonaContext {
|
|
61
|
+
ensureDir();
|
|
62
|
+
try {
|
|
63
|
+
if (existsSync(PERSONA_FILE)) {
|
|
64
|
+
const data = JSON.parse(readFileSync(PERSONA_FILE, 'utf-8'));
|
|
65
|
+
if (data.userId === userId) {
|
|
66
|
+
data.sessions++;
|
|
67
|
+
data.lastSeen = new Date().toISOString();
|
|
68
|
+
writeFileSync(PERSONA_FILE, JSON.stringify(data, null, 2));
|
|
69
|
+
return data;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
// corrupt persona file — create fresh one below
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const persona = emptyPersona(userId, userName);
|
|
77
|
+
writeFileSync(PERSONA_FILE, JSON.stringify(persona, null, 2));
|
|
78
|
+
return persona;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function savePersona(persona: PersonaContext): void {
|
|
82
|
+
ensureDir();
|
|
83
|
+
writeFileSync(PERSONA_FILE, JSON.stringify(persona, null, 2));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function updatePersona(updates: Partial<PersonaContext>): PersonaContext {
|
|
87
|
+
ensureDir();
|
|
88
|
+
let current: PersonaContext;
|
|
89
|
+
try {
|
|
90
|
+
current = JSON.parse(readFileSync(PERSONA_FILE, 'utf-8'));
|
|
91
|
+
} catch {
|
|
92
|
+
current = emptyPersona(updates.userId || 'user', updates.userName || 'User');
|
|
93
|
+
}
|
|
94
|
+
const merged = { ...current, ...updates, lastSeen: new Date().toISOString() };
|
|
95
|
+
writeFileSync(PERSONA_FILE, JSON.stringify(merged, null, 2));
|
|
96
|
+
return merged;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function buildPersonaBlock(persona: PersonaContext): string {
|
|
100
|
+
const remember = persona.memorableMoments.length
|
|
101
|
+
? `\nMemorable moments: ${persona.memorableMoments.slice(-3).join('; ')}`
|
|
102
|
+
: '\nBuilding history together.';
|
|
103
|
+
const jokes = persona.insideJokes.length
|
|
104
|
+
? `\nInside jokes: ${persona.insideJokes.slice(-3).join('; ')}`
|
|
105
|
+
: '';
|
|
106
|
+
return `[PERSONA CONTINUITY — session ${persona.sessions}]
|
|
107
|
+
You are speaking to ${persona.userName}. This is your ${persona.sessions}th conversation.
|
|
108
|
+
You've exchanged ${persona.totalMessages} messages since ${new Date(persona.relationshipStarted).toLocaleDateString()}.
|
|
109
|
+
Preferred tone: ${persona.preferredTone}.
|
|
110
|
+
Known interests: ${persona.knownInterests.join(', ') || 'still learning'}.
|
|
111
|
+
Current project: ${persona.projectContext.currentProject || 'exploring'}.
|
|
112
|
+
Tech stack: ${persona.projectContext.techStack.join(', ') || 'scanning'}.${remember}${jokes}`;
|
|
113
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { Message, ChatOptions, ChatResult } from './types.js';
|
|
2
|
+
|
|
3
|
+
export async function chat(
|
|
4
|
+
messages: Message[],
|
|
5
|
+
apiKey: string,
|
|
6
|
+
model: string,
|
|
7
|
+
opts?: ChatOptions,
|
|
8
|
+
): Promise<ChatResult> {
|
|
9
|
+
if (opts?.stream) {
|
|
10
|
+
return streamChat(messages, apiKey, model);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const resp = await fetch('https://api.anthropic.com/v1/messages', {
|
|
14
|
+
method: 'POST',
|
|
15
|
+
headers: {
|
|
16
|
+
'Content-Type': 'application/json',
|
|
17
|
+
'x-api-key': apiKey,
|
|
18
|
+
'anthropic-version': '2023-06-01',
|
|
19
|
+
},
|
|
20
|
+
body: JSON.stringify({
|
|
21
|
+
model: model || 'claude-sonnet-4-20250514',
|
|
22
|
+
max_tokens: opts?.maxTokens ?? 4096,
|
|
23
|
+
messages: messages.filter((m) => m.role !== 'system'),
|
|
24
|
+
system: messages
|
|
25
|
+
.filter((m) => m.role === 'system')
|
|
26
|
+
.map((m) => ({ type: 'text' as const, text: m.content })),
|
|
27
|
+
}),
|
|
28
|
+
signal: opts?.signal,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (!resp.ok) {
|
|
32
|
+
const err = await resp.text().catch(() => resp.statusText);
|
|
33
|
+
throw new Error(`Anthropic error ${resp.status}: ${err}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const data = (await resp.json()) as {
|
|
37
|
+
content: { type: string; text: string }[];
|
|
38
|
+
usage?: { input_tokens: number; output_tokens: number };
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const text = data.content
|
|
42
|
+
.filter((c) => c.type === 'text')
|
|
43
|
+
.map((c) => c.text)
|
|
44
|
+
.join('');
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
text,
|
|
48
|
+
usage: data.usage
|
|
49
|
+
? { promptTokens: data.usage.input_tokens, completionTokens: data.usage.output_tokens }
|
|
50
|
+
: undefined,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function streamChat(
|
|
55
|
+
messages: Message[],
|
|
56
|
+
apiKey: string,
|
|
57
|
+
model: string,
|
|
58
|
+
onToken?: (token: string) => void,
|
|
59
|
+
): Promise<ChatResult> {
|
|
60
|
+
const resp = await fetch('https://api.anthropic.com/v1/messages', {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: {
|
|
63
|
+
'Content-Type': 'application/json',
|
|
64
|
+
'x-api-key': apiKey,
|
|
65
|
+
'anthropic-version': '2023-06-01',
|
|
66
|
+
},
|
|
67
|
+
body: JSON.stringify({
|
|
68
|
+
model: model || 'claude-sonnet-4-20250514',
|
|
69
|
+
max_tokens: 4096,
|
|
70
|
+
messages: messages.filter((m) => m.role !== 'system'),
|
|
71
|
+
system: messages
|
|
72
|
+
.filter((m) => m.role === 'system')
|
|
73
|
+
.map((m) => ({ type: 'text' as const, text: m.content })),
|
|
74
|
+
stream: true,
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (!resp.ok) {
|
|
79
|
+
const err = await resp.text().catch(() => resp.statusText);
|
|
80
|
+
throw new Error(`Anthropic error ${resp.status}: ${err}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const reader = resp.body?.getReader();
|
|
84
|
+
if (!reader) throw new Error('No response body');
|
|
85
|
+
|
|
86
|
+
let fullText = '';
|
|
87
|
+
const decoder = new TextDecoder();
|
|
88
|
+
let buffer = '';
|
|
89
|
+
|
|
90
|
+
while (true) {
|
|
91
|
+
const { done, value } = await reader.read();
|
|
92
|
+
if (done) break;
|
|
93
|
+
|
|
94
|
+
buffer += decoder.decode(value, { stream: true });
|
|
95
|
+
const lines = buffer.split('\n');
|
|
96
|
+
buffer = lines.pop() ?? '';
|
|
97
|
+
|
|
98
|
+
for (const line of lines) {
|
|
99
|
+
const trimmed = line.trim();
|
|
100
|
+
if (!trimmed.startsWith('data: ')) continue;
|
|
101
|
+
const json = trimmed.slice(6);
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const parsed = JSON.parse(json) as {
|
|
105
|
+
type: string;
|
|
106
|
+
delta?: { text?: string };
|
|
107
|
+
content_block?: { text?: string };
|
|
108
|
+
};
|
|
109
|
+
if (parsed.type === 'content_block_delta' && parsed.delta?.text) {
|
|
110
|
+
fullText += parsed.delta.text;
|
|
111
|
+
onToken?.(parsed.delta.text);
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
// skip malformed chunks
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { text: fullText };
|
|
120
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Message, ChatOptions, ChatResult } from './types.js';
|
|
2
|
+
|
|
3
|
+
export async function chat(
|
|
4
|
+
messages: Message[],
|
|
5
|
+
apiKey: string,
|
|
6
|
+
model: string,
|
|
7
|
+
opts?: ChatOptions,
|
|
8
|
+
): Promise<ChatResult> {
|
|
9
|
+
const resp = await fetch('https://api.deepseek.com/chat/completions', {
|
|
10
|
+
method: 'POST',
|
|
11
|
+
headers: {
|
|
12
|
+
'Content-Type': 'application/json',
|
|
13
|
+
Authorization: `Bearer ${apiKey}`,
|
|
14
|
+
},
|
|
15
|
+
body: JSON.stringify({
|
|
16
|
+
model: model || 'deepseek-chat',
|
|
17
|
+
messages,
|
|
18
|
+
max_tokens: opts?.maxTokens ?? 4096,
|
|
19
|
+
stream: false,
|
|
20
|
+
}),
|
|
21
|
+
signal: opts?.signal,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (!resp.ok) {
|
|
25
|
+
const err = await resp.text().catch(() => resp.statusText);
|
|
26
|
+
throw new Error(`DeepSeek error ${resp.status}: ${err}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const data = (await resp.json()) as {
|
|
30
|
+
choices: { message: { content: string } }[];
|
|
31
|
+
usage?: { prompt_tokens: number; completion_tokens: number };
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
text: data.choices[0]?.message?.content ?? '',
|
|
36
|
+
usage: data.usage
|
|
37
|
+
? { promptTokens: data.usage.prompt_tokens, completionTokens: data.usage.completion_tokens }
|
|
38
|
+
: undefined,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Message, ChatOptions, ChatResult } from './types.js';
|
|
2
|
+
|
|
3
|
+
export async function chat(
|
|
4
|
+
messages: Message[],
|
|
5
|
+
apiKey: string,
|
|
6
|
+
model: string,
|
|
7
|
+
opts?: ChatOptions,
|
|
8
|
+
): Promise<ChatResult> {
|
|
9
|
+
const contents = messages.map((m) => ({
|
|
10
|
+
role: m.role === 'assistant' ? 'model' : 'user',
|
|
11
|
+
parts: [{ text: m.content }],
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
const systemMessage = messages.find((m) => m.role === 'system');
|
|
15
|
+
const systemInstruction = systemMessage
|
|
16
|
+
? { parts: [{ text: systemMessage.content }] }
|
|
17
|
+
: undefined;
|
|
18
|
+
|
|
19
|
+
const resp = await fetch(
|
|
20
|
+
`https://generativelanguage.googleapis.com/v1beta/models/${model || 'gemini-2.5-pro-preview-05-06'}:generateContent?key=${apiKey}`,
|
|
21
|
+
{
|
|
22
|
+
method: 'POST',
|
|
23
|
+
headers: { 'Content-Type': 'application/json' },
|
|
24
|
+
body: JSON.stringify({
|
|
25
|
+
contents,
|
|
26
|
+
system_instruction: systemInstruction,
|
|
27
|
+
generationConfig: {
|
|
28
|
+
maxOutputTokens: opts?.maxTokens ?? 4096,
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
signal: opts?.signal,
|
|
32
|
+
},
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
if (!resp.ok) {
|
|
36
|
+
const err = await resp.text().catch(() => resp.statusText);
|
|
37
|
+
throw new Error(`Google error ${resp.status}: ${err}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const data = (await resp.json()) as {
|
|
41
|
+
candidates?: { content?: { parts?: { text: string }[] } }[];
|
|
42
|
+
usageMetadata?: { promptTokenCount: number; candidatesTokenCount: number };
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const text =
|
|
46
|
+
data.candidates?.[0]?.content?.parts?.map((p) => p.text).join('') ?? '';
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
text,
|
|
50
|
+
usage: data.usageMetadata
|
|
51
|
+
? {
|
|
52
|
+
promptTokens: data.usageMetadata.promptTokenCount,
|
|
53
|
+
completionTokens: data.usageMetadata.candidatesTokenCount,
|
|
54
|
+
}
|
|
55
|
+
: undefined,
|
|
56
|
+
};
|
|
57
|
+
}
|