@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.
Files changed (153) hide show
  1. package/bin/aria.js +168 -0
  2. package/dist/aria-connector/src/auth-commands.d.ts +28 -0
  3. package/dist/aria-connector/src/auth-commands.d.ts.map +1 -0
  4. package/dist/aria-connector/src/auth-commands.js +129 -0
  5. package/dist/aria-connector/src/auth-commands.js.map +1 -0
  6. package/dist/aria-connector/src/auth.d.ts +12 -0
  7. package/dist/aria-connector/src/auth.d.ts.map +1 -0
  8. package/dist/aria-connector/src/auth.js +31 -0
  9. package/dist/aria-connector/src/auth.js.map +1 -0
  10. package/dist/aria-connector/src/auto-mcp.d.ts +23 -0
  11. package/dist/aria-connector/src/auto-mcp.d.ts.map +1 -0
  12. package/dist/aria-connector/src/auto-mcp.js +994 -0
  13. package/dist/aria-connector/src/auto-mcp.js.map +1 -0
  14. package/dist/aria-connector/src/chat.d.ts +21 -0
  15. package/dist/aria-connector/src/chat.d.ts.map +1 -0
  16. package/dist/aria-connector/src/chat.js +332 -0
  17. package/dist/aria-connector/src/chat.js.map +1 -0
  18. package/dist/aria-connector/src/codebase-scanner.d.ts +7 -0
  19. package/dist/aria-connector/src/codebase-scanner.d.ts.map +1 -0
  20. package/dist/aria-connector/src/codebase-scanner.js +6 -0
  21. package/dist/aria-connector/src/codebase-scanner.js.map +1 -0
  22. package/dist/aria-connector/src/cognition-log.d.ts +17 -0
  23. package/dist/aria-connector/src/cognition-log.d.ts.map +1 -0
  24. package/dist/aria-connector/src/cognition-log.js +19 -0
  25. package/dist/aria-connector/src/cognition-log.js.map +1 -0
  26. package/dist/aria-connector/src/config.d.ts +41 -0
  27. package/dist/aria-connector/src/config.d.ts.map +1 -0
  28. package/dist/aria-connector/src/config.js +50 -0
  29. package/dist/aria-connector/src/config.js.map +1 -0
  30. package/dist/aria-connector/src/connectors/claude-code.d.ts +4 -0
  31. package/dist/aria-connector/src/connectors/claude-code.d.ts.map +1 -0
  32. package/dist/aria-connector/src/connectors/claude-code.js +204 -0
  33. package/dist/aria-connector/src/connectors/claude-code.js.map +1 -0
  34. package/dist/aria-connector/src/connectors/cursor.d.ts +4 -0
  35. package/dist/aria-connector/src/connectors/cursor.d.ts.map +1 -0
  36. package/dist/aria-connector/src/connectors/cursor.js +63 -0
  37. package/dist/aria-connector/src/connectors/cursor.js.map +1 -0
  38. package/dist/aria-connector/src/connectors/opencode.d.ts +4 -0
  39. package/dist/aria-connector/src/connectors/opencode.d.ts.map +1 -0
  40. package/dist/aria-connector/src/connectors/opencode.js +102 -0
  41. package/dist/aria-connector/src/connectors/opencode.js.map +1 -0
  42. package/dist/aria-connector/src/connectors/shell.d.ts +4 -0
  43. package/dist/aria-connector/src/connectors/shell.d.ts.map +1 -0
  44. package/dist/aria-connector/src/connectors/shell.js +58 -0
  45. package/dist/aria-connector/src/connectors/shell.js.map +1 -0
  46. package/dist/aria-connector/src/garden-client.d.ts +19 -0
  47. package/dist/aria-connector/src/garden-client.d.ts.map +1 -0
  48. package/dist/aria-connector/src/garden-client.js +85 -0
  49. package/dist/aria-connector/src/garden-client.js.map +1 -0
  50. package/dist/aria-connector/src/garden-control-plane.d.ts +22 -0
  51. package/dist/aria-connector/src/garden-control-plane.d.ts.map +1 -0
  52. package/dist/aria-connector/src/garden-control-plane.js +43 -0
  53. package/dist/aria-connector/src/garden-control-plane.js.map +1 -0
  54. package/dist/aria-connector/src/harness-client.d.ts +166 -0
  55. package/dist/aria-connector/src/harness-client.d.ts.map +1 -0
  56. package/dist/aria-connector/src/harness-client.js +344 -0
  57. package/dist/aria-connector/src/harness-client.js.map +1 -0
  58. package/dist/aria-connector/src/hive-client.d.ts +32 -0
  59. package/dist/aria-connector/src/hive-client.d.ts.map +1 -0
  60. package/dist/aria-connector/src/hive-client.js +69 -0
  61. package/dist/aria-connector/src/hive-client.js.map +1 -0
  62. package/dist/aria-connector/src/index.d.ts +19 -0
  63. package/dist/aria-connector/src/index.d.ts.map +1 -0
  64. package/dist/aria-connector/src/index.js +13 -0
  65. package/dist/aria-connector/src/index.js.map +1 -0
  66. package/dist/aria-connector/src/install-hooks.d.ts +18 -0
  67. package/dist/aria-connector/src/install-hooks.d.ts.map +1 -0
  68. package/dist/aria-connector/src/install-hooks.js +224 -0
  69. package/dist/aria-connector/src/install-hooks.js.map +1 -0
  70. package/dist/aria-connector/src/model-context.d.ts +8 -0
  71. package/dist/aria-connector/src/model-context.d.ts.map +1 -0
  72. package/dist/aria-connector/src/model-context.js +83 -0
  73. package/dist/aria-connector/src/model-context.js.map +1 -0
  74. package/dist/aria-connector/src/persona.d.ts +27 -0
  75. package/dist/aria-connector/src/persona.d.ts.map +1 -0
  76. package/dist/aria-connector/src/persona.js +86 -0
  77. package/dist/aria-connector/src/persona.js.map +1 -0
  78. package/dist/aria-connector/src/providers/anthropic.d.ts +4 -0
  79. package/dist/aria-connector/src/providers/anthropic.d.ts.map +1 -0
  80. package/dist/aria-connector/src/providers/anthropic.js +92 -0
  81. package/dist/aria-connector/src/providers/anthropic.js.map +1 -0
  82. package/dist/aria-connector/src/providers/deepseek.d.ts +3 -0
  83. package/dist/aria-connector/src/providers/deepseek.d.ts.map +1 -0
  84. package/dist/aria-connector/src/providers/deepseek.js +28 -0
  85. package/dist/aria-connector/src/providers/deepseek.js.map +1 -0
  86. package/dist/aria-connector/src/providers/google.d.ts +3 -0
  87. package/dist/aria-connector/src/providers/google.d.ts.map +1 -0
  88. package/dist/aria-connector/src/providers/google.js +38 -0
  89. package/dist/aria-connector/src/providers/google.js.map +1 -0
  90. package/dist/aria-connector/src/providers/ollama.d.ts +3 -0
  91. package/dist/aria-connector/src/providers/ollama.d.ts.map +1 -0
  92. package/dist/aria-connector/src/providers/ollama.js +28 -0
  93. package/dist/aria-connector/src/providers/ollama.js.map +1 -0
  94. package/dist/aria-connector/src/providers/openai.d.ts +4 -0
  95. package/dist/aria-connector/src/providers/openai.d.ts.map +1 -0
  96. package/dist/aria-connector/src/providers/openai.js +84 -0
  97. package/dist/aria-connector/src/providers/openai.js.map +1 -0
  98. package/dist/aria-connector/src/providers/openrouter.d.ts +3 -0
  99. package/dist/aria-connector/src/providers/openrouter.d.ts.map +1 -0
  100. package/dist/aria-connector/src/providers/openrouter.js +30 -0
  101. package/dist/aria-connector/src/providers/openrouter.js.map +1 -0
  102. package/dist/aria-connector/src/providers/types.d.ts +20 -0
  103. package/dist/aria-connector/src/providers/types.d.ts.map +1 -0
  104. package/dist/aria-connector/src/providers/types.js +2 -0
  105. package/dist/aria-connector/src/providers/types.js.map +1 -0
  106. package/dist/aria-connector/src/setup-wizard.d.ts +2 -0
  107. package/dist/aria-connector/src/setup-wizard.d.ts.map +1 -0
  108. package/dist/aria-connector/src/setup-wizard.js +140 -0
  109. package/dist/aria-connector/src/setup-wizard.js.map +1 -0
  110. package/dist/aria-connector/src/types.d.ts +30 -0
  111. package/dist/aria-connector/src/types.d.ts.map +1 -0
  112. package/dist/aria-connector/src/types.js +5 -0
  113. package/dist/aria-connector/src/types.js.map +1 -0
  114. package/dist/aria-web/src/lib/codebase-scanner.d.ts +127 -0
  115. package/dist/aria-web/src/lib/codebase-scanner.d.ts.map +1 -0
  116. package/dist/aria-web/src/lib/codebase-scanner.js +1730 -0
  117. package/dist/aria-web/src/lib/codebase-scanner.js.map +1 -0
  118. package/dist/cli-0.2.0.tgz +0 -0
  119. package/dist/install.sh +13 -0
  120. package/hooks/aria-harness-via-sdk.mjs +317 -0
  121. package/hooks/aria-pre-tool-gate.mjs +596 -0
  122. package/hooks/aria-preprompt-consult.mjs +175 -0
  123. package/hooks/aria-stop-gate.mjs +222 -0
  124. package/package.json +47 -0
  125. package/src/__tests__/auth-commands.test.ts +132 -0
  126. package/src/auth-commands.ts +175 -0
  127. package/src/auth.ts +33 -0
  128. package/src/auto-mcp.ts +1172 -0
  129. package/src/chat.ts +387 -0
  130. package/src/codebase-scanner.ts +18 -0
  131. package/src/cognition-log.ts +30 -0
  132. package/src/config.ts +94 -0
  133. package/src/connectors/claude-code.ts +213 -0
  134. package/src/connectors/cursor.ts +75 -0
  135. package/src/connectors/opencode.ts +115 -0
  136. package/src/connectors/shell.ts +72 -0
  137. package/src/garden-client.ts +98 -0
  138. package/src/garden-control-plane.ts +108 -0
  139. package/src/harness-client.ts +454 -0
  140. package/src/hive-client.ts +104 -0
  141. package/src/index.ts +26 -0
  142. package/src/install-hooks.ts +259 -0
  143. package/src/model-context.ts +88 -0
  144. package/src/persona.ts +113 -0
  145. package/src/providers/anthropic.ts +120 -0
  146. package/src/providers/deepseek.ts +40 -0
  147. package/src/providers/google.ts +57 -0
  148. package/src/providers/ollama.ts +43 -0
  149. package/src/providers/openai.ts +108 -0
  150. package/src/providers/openrouter.ts +42 -0
  151. package/src/providers/types.ts +35 -0
  152. package/src/setup-wizard.ts +177 -0
  153. 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
+ }