@aria_asi/cli 0.2.36 → 0.2.37

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 (195) hide show
  1. package/CLIENT-ONBOARDING.md +4 -2
  2. package/bin/aria.js +11 -7
  3. package/dist/aria-connector/src/auth.d.ts +14 -0
  4. package/dist/aria-connector/src/auth.d.ts.map +1 -1
  5. package/dist/aria-connector/src/auth.js +103 -1
  6. package/dist/aria-connector/src/auth.js.map +1 -1
  7. package/dist/aria-connector/src/chat.d.ts.map +1 -1
  8. package/dist/aria-connector/src/chat.js +13 -8
  9. package/dist/aria-connector/src/chat.js.map +1 -1
  10. package/dist/aria-connector/src/config.d.ts +6 -1
  11. package/dist/aria-connector/src/config.d.ts.map +1 -1
  12. package/dist/aria-connector/src/config.js.map +1 -1
  13. package/dist/aria-connector/src/connectors/claude-code.d.ts.map +1 -1
  14. package/dist/aria-connector/src/connectors/claude-code.js +50 -6
  15. package/dist/aria-connector/src/connectors/claude-code.js.map +1 -1
  16. package/dist/aria-connector/src/connectors/codex.d.ts.map +1 -1
  17. package/dist/aria-connector/src/connectors/codex.js +310 -10
  18. package/dist/aria-connector/src/connectors/codex.js.map +1 -1
  19. package/dist/aria-connector/src/connectors/opencode.d.ts.map +1 -1
  20. package/dist/aria-connector/src/connectors/opencode.js +35 -11
  21. package/dist/aria-connector/src/connectors/opencode.js.map +1 -1
  22. package/dist/aria-connector/src/connectors/repo-guard.d.ts +10 -0
  23. package/dist/aria-connector/src/connectors/repo-guard.d.ts.map +1 -1
  24. package/dist/aria-connector/src/connectors/repo-guard.js +110 -164
  25. package/dist/aria-connector/src/connectors/repo-guard.js.map +1 -1
  26. package/dist/aria-connector/src/connectors/runtime.d.ts.map +1 -1
  27. package/dist/aria-connector/src/connectors/runtime.js +17 -7
  28. package/dist/aria-connector/src/connectors/runtime.js.map +1 -1
  29. package/dist/aria-connector/src/connectors/shell.d.ts.map +1 -1
  30. package/dist/aria-connector/src/connectors/shell.js +12 -8
  31. package/dist/aria-connector/src/connectors/shell.js.map +1 -1
  32. package/dist/aria-connector/src/harness-client.d.ts +3 -1
  33. package/dist/aria-connector/src/harness-client.d.ts.map +1 -1
  34. package/dist/aria-connector/src/harness-client.js +7 -20
  35. package/dist/aria-connector/src/harness-client.js.map +1 -1
  36. package/dist/aria-connector/src/model-context.d.ts.map +1 -1
  37. package/dist/aria-connector/src/model-context.js +5 -0
  38. package/dist/aria-connector/src/model-context.js.map +1 -1
  39. package/dist/aria-connector/src/providers/types.d.ts +1 -1
  40. package/dist/aria-connector/src/providers/types.d.ts.map +1 -1
  41. package/dist/aria-connector/src/providers/xai.d.ts +3 -0
  42. package/dist/aria-connector/src/providers/xai.d.ts.map +1 -0
  43. package/dist/aria-connector/src/providers/xai.js +40 -0
  44. package/dist/aria-connector/src/providers/xai.js.map +1 -0
  45. package/dist/aria-connector/src/setup-wizard.js +1 -0
  46. package/dist/aria-connector/src/setup-wizard.js.map +1 -1
  47. package/dist/aria-connector/src/types.d.ts +2 -0
  48. package/dist/aria-connector/src/types.d.ts.map +1 -1
  49. package/dist/assets/hooks/aria-cognition-substrate-binding.mjs +51 -9
  50. package/dist/assets/hooks/aria-first-class-coach.mjs +129 -0
  51. package/dist/assets/hooks/aria-harness-via-sdk.mjs +33 -6
  52. package/dist/assets/hooks/aria-pre-tool-gate.mjs +33 -8
  53. package/dist/assets/hooks/aria-preprompt-consult.mjs +5 -6
  54. package/dist/assets/hooks/aria-preturn-memory-gate.mjs +5 -0
  55. package/dist/assets/hooks/aria-repo-doctrine-gate.mjs +15 -0
  56. package/dist/assets/hooks/aria-stop-gate.mjs +125 -17
  57. package/dist/assets/hooks/doctrine_trigger_map.json +11 -0
  58. package/dist/assets/hooks/lib/emergency-gateoff-impl.mjs +39 -0
  59. package/dist/assets/hooks/lib/emergency-gateoff.mjs +6 -0
  60. package/dist/assets/hooks/lib/first-class-coach.mjs +755 -0
  61. package/dist/assets/hooks/lib/skill-autoload-gate-impl.mjs +103 -0
  62. package/dist/assets/hooks/lib/skill-autoload-gate.mjs +1 -14
  63. package/dist/assets/opencode-plugins/harness-context/auth-token.mjs +126 -0
  64. package/dist/assets/opencode-plugins/harness-context/inject-context.mjs +62 -22
  65. package/dist/assets/opencode-plugins/harness-context/task-project-ledger.mjs +290 -0
  66. package/dist/assets/opencode-plugins/harness-gate/index.js +87 -27
  67. package/dist/assets/opencode-plugins/harness-gate/lib/skill-autoload-gate.js +1 -14
  68. package/dist/assets/opencode-plugins/harness-outcome/index.js +29 -24
  69. package/dist/assets/opencode-plugins/harness-stop/index.js +229 -68
  70. package/dist/assets/opencode-plugins/harness-stop/lib/skill-autoload-gate.js +1 -14
  71. package/dist/runtime/auth-token.mjs +121 -0
  72. package/dist/runtime/coach-kernel.mjs +371 -0
  73. package/dist/runtime/codex-bridge.mjs +440 -69
  74. package/dist/runtime/discipline/doctrine_trigger_map.json +11 -0
  75. package/dist/runtime/discipline/skills/aria-cognition/aria-essence/SKILL.md +18 -0
  76. package/dist/runtime/discipline/skills/aria-cognition/aria-forge-guardrails/SKILL.md +18 -0
  77. package/dist/runtime/discipline/skills/aria-cognition/aria-repo-doctrine/SKILL.md +18 -0
  78. package/dist/runtime/discipline/skills/aria-cognition/forge-quality-rules/SKILL.md +18 -0
  79. package/dist/runtime/discipline/skills/aria-cognition/ghazali-8lens/SKILL.md +18 -0
  80. package/dist/runtime/discipline/skills/aria-cognition/istiqra-induction/SKILL.md +18 -0
  81. package/dist/runtime/discipline/skills/aria-cognition/ladunni-22/SKILL.md +18 -0
  82. package/dist/runtime/discipline/skills/aria-cognition/mizan/SKILL.md +18 -0
  83. package/dist/runtime/discipline/skills/aria-cognition/nadia/SKILL.md +18 -0
  84. package/dist/runtime/discipline/skills/aria-cognition/nadia-psi/SKILL.md +18 -0
  85. package/dist/runtime/discipline/skills/aria-cognition/predictor/SKILL.md +18 -0
  86. package/dist/runtime/discipline/skills/aria-cognition/qiyas-analogy/SKILL.md +18 -0
  87. package/dist/runtime/discipline/skills/aria-cognition/soul-domains/SKILL.md +18 -0
  88. package/dist/runtime/discipline/skills/aria-harness/aria-aristotle-intra-phase/SKILL.md +18 -0
  89. package/dist/runtime/discipline/skills/aria-harness/aria-aristotle-post-phase/SKILL.md +18 -0
  90. package/dist/runtime/discipline/skills/aria-harness/aria-aristotle-pre-phase/SKILL.md +18 -0
  91. package/dist/runtime/discipline/skills/aria-harness/aria-harness-deploy/SKILL.md +18 -0
  92. package/dist/runtime/discipline/skills/aria-harness/aria-harness-no-stripping/SKILL.md +18 -0
  93. package/dist/runtime/discipline/skills/aria-harness/aria-harness-onboarding/SKILL.md +18 -0
  94. package/dist/runtime/discipline/skills/aria-harness/aria-harness-output-discipline/SKILL.md +18 -0
  95. package/dist/runtime/discipline/skills/aria-harness/aria-harness-substrate-binding/SKILL.md +18 -0
  96. package/dist/runtime/doctrine_trigger_map.json +11 -0
  97. package/dist/runtime/hooks/aria-cognition-substrate-binding.mjs +51 -9
  98. package/dist/runtime/hooks/aria-first-class-coach.mjs +129 -0
  99. package/dist/runtime/hooks/aria-harness-via-sdk.mjs +33 -6
  100. package/dist/runtime/hooks/aria-pre-tool-gate.mjs +33 -8
  101. package/dist/runtime/hooks/aria-preprompt-consult.mjs +5 -6
  102. package/dist/runtime/hooks/aria-preturn-memory-gate.mjs +5 -0
  103. package/dist/runtime/hooks/aria-repo-doctrine-gate.mjs +15 -0
  104. package/dist/runtime/hooks/aria-stop-gate.mjs +125 -17
  105. package/dist/runtime/hooks/doctrine_trigger_map.json +11 -0
  106. package/dist/runtime/hooks/lib/emergency-gateoff-impl.mjs +39 -0
  107. package/dist/runtime/hooks/lib/emergency-gateoff.mjs +6 -0
  108. package/dist/runtime/hooks/lib/first-class-coach.mjs +755 -0
  109. package/dist/runtime/hooks/lib/skill-autoload-gate-impl.mjs +103 -0
  110. package/dist/runtime/hooks/lib/skill-autoload-gate.mjs +1 -14
  111. package/dist/runtime/local-phase.mjs +8 -0
  112. package/dist/runtime/manifest.json +2 -2
  113. package/dist/runtime/provider-proxy.mjs +136 -33
  114. package/dist/runtime/sdk/BUNDLED.json +2 -2
  115. package/dist/runtime/sdk/auth.d.ts +17 -0
  116. package/dist/runtime/sdk/auth.js +158 -0
  117. package/dist/runtime/sdk/auth.js.map +1 -0
  118. package/dist/runtime/sdk/index.d.ts +8 -1
  119. package/dist/runtime/sdk/index.js +15 -1
  120. package/dist/runtime/sdk/index.js.map +1 -1
  121. package/dist/runtime/service.mjs +1711 -74
  122. package/dist/runtime/task-project-ledger.mjs +290 -0
  123. package/dist/sdk/BUNDLED.json +2 -2
  124. package/dist/sdk/auth.d.ts +17 -0
  125. package/dist/sdk/auth.js +158 -0
  126. package/dist/sdk/auth.js.map +1 -0
  127. package/dist/sdk/index.d.ts +8 -1
  128. package/dist/sdk/index.js +15 -1
  129. package/dist/sdk/index.js.map +1 -1
  130. package/hooks/aria-cognition-substrate-binding.mjs +51 -9
  131. package/hooks/aria-first-class-coach.mjs +129 -0
  132. package/hooks/aria-harness-via-sdk.mjs +33 -6
  133. package/hooks/aria-pre-tool-gate.mjs +33 -8
  134. package/hooks/aria-preprompt-consult.mjs +5 -6
  135. package/hooks/aria-preturn-memory-gate.mjs +5 -0
  136. package/hooks/aria-repo-doctrine-gate.mjs +15 -0
  137. package/hooks/aria-stop-gate.mjs +125 -17
  138. package/hooks/doctrine_trigger_map.json +11 -0
  139. package/hooks/lib/emergency-gateoff-impl.mjs +39 -0
  140. package/hooks/lib/emergency-gateoff.mjs +6 -0
  141. package/hooks/lib/first-class-coach.mjs +755 -0
  142. package/hooks/lib/skill-autoload-gate-impl.mjs +103 -0
  143. package/hooks/lib/skill-autoload-gate.mjs +1 -14
  144. package/opencode-plugins/harness-context/auth-token.mjs +126 -0
  145. package/opencode-plugins/harness-context/inject-context.mjs +62 -22
  146. package/opencode-plugins/harness-context/task-project-ledger.mjs +290 -0
  147. package/opencode-plugins/harness-gate/index.js +87 -27
  148. package/opencode-plugins/harness-gate/lib/skill-autoload-gate.js +1 -14
  149. package/opencode-plugins/harness-outcome/index.js +29 -24
  150. package/opencode-plugins/harness-stop/index.js +229 -68
  151. package/opencode-plugins/harness-stop/lib/skill-autoload-gate.js +1 -14
  152. package/package.json +8 -2
  153. package/runtime-src/auth-token.mjs +121 -0
  154. package/runtime-src/coach-kernel.mjs +371 -0
  155. package/runtime-src/codex-bridge.mjs +440 -69
  156. package/runtime-src/local-phase.mjs +8 -0
  157. package/runtime-src/provider-proxy.mjs +136 -33
  158. package/runtime-src/service.mjs +1711 -74
  159. package/scripts/bundle-sdk.mjs +8 -0
  160. package/scripts/check-client-compatibility.mjs +422 -0
  161. package/scripts/check-coach-kernel.mjs +204 -0
  162. package/scripts/check-managed-runtime-ledger.mjs +107 -0
  163. package/scripts/check-opencode-config-contract.mjs +78 -0
  164. package/scripts/check-quality-ledger.mjs +121 -0
  165. package/scripts/self-test-harness-gates.mjs +179 -11
  166. package/scripts/self-test-repo-guard.mjs +38 -0
  167. package/scripts/validate-skill-prompts.mjs +14 -1
  168. package/skills/aria-cognition/aria-essence/SKILL.md +18 -0
  169. package/skills/aria-cognition/aria-forge-guardrails/SKILL.md +18 -0
  170. package/skills/aria-cognition/aria-repo-doctrine/SKILL.md +18 -0
  171. package/skills/aria-cognition/forge-quality-rules/SKILL.md +18 -0
  172. package/skills/aria-cognition/ghazali-8lens/SKILL.md +18 -0
  173. package/skills/aria-cognition/istiqra-induction/SKILL.md +18 -0
  174. package/skills/aria-cognition/ladunni-22/SKILL.md +18 -0
  175. package/skills/aria-cognition/mizan/SKILL.md +18 -0
  176. package/skills/aria-cognition/nadia/SKILL.md +18 -0
  177. package/skills/aria-cognition/nadia-psi/SKILL.md +18 -0
  178. package/skills/aria-cognition/predictor/SKILL.md +18 -0
  179. package/skills/aria-cognition/qiyas-analogy/SKILL.md +18 -0
  180. package/skills/aria-cognition/soul-domains/SKILL.md +18 -0
  181. package/src/auth.ts +136 -1
  182. package/src/chat.ts +13 -8
  183. package/src/config.ts +6 -1
  184. package/src/connectors/claude-code.ts +62 -18
  185. package/src/connectors/codex.ts +308 -10
  186. package/src/connectors/opencode.ts +35 -12
  187. package/src/connectors/repo-guard.ts +117 -172
  188. package/src/connectors/runtime.ts +19 -7
  189. package/src/connectors/shell.ts +12 -8
  190. package/src/harness-client.ts +8 -22
  191. package/src/model-context.ts +6 -0
  192. package/src/providers/types.ts +1 -1
  193. package/src/providers/xai.ts +55 -0
  194. package/src/setup-wizard.ts +1 -0
  195. package/src/types.ts +2 -0
@@ -1,13 +1,10 @@
1
1
  import chokidar from 'chokidar';
2
2
  import { execFileSync } from 'child_process';
3
- import { createHash } from 'crypto';
4
3
  import {
5
4
  existsSync,
6
5
  mkdirSync,
7
- readdirSync,
8
6
  readFileSync,
9
7
  statSync,
10
- renameSync,
11
8
  writeFileSync,
12
9
  } from 'fs';
13
10
  import { homedir } from 'os';
@@ -22,17 +19,10 @@ const ENV_PATH = path.join(ARIA_DIR, 'repo-guard.env');
22
19
  const STATE_PATH = path.join(ARIA_DIR, 'repo-guard-state.json');
23
20
  const EVENTS_PATH = path.join(ARIA_DIR, 'repo-guard-events.jsonl');
24
21
  const LAST_ALERT_PATH = path.join(ARIA_DIR, 'repo-guard-last-alert.json');
25
- const SHADOW_ROOT = path.join(ARIA_DIR, 'repo-guard', 'shadow');
22
+ const APPROVALS_PATH = path.join(ARIA_DIR, 'repo-guard-approvals.json');
26
23
  const QUARANTINE_ROOT = path.join(ARIA_DIR, 'repo-guard', 'quarantine');
27
24
  const DEFAULT_DEBOUNCE_MS = Number(process.env.ARIA_REPO_GUARD_DEBOUNCE_MS || 350);
28
25
  const BLOCKING_MODE = !/^(0|false|no)$/i.test(process.env.ARIA_REPO_GUARD_BLOCKING || 'true');
29
- const DOCTRINE_PREFIXES = ['apps/', 'packages/', 'harness/', 'ops/', 'scripts/'];
30
- const DOCTRINE_EXTENSIONS = new Set([
31
- '.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs',
32
- '.py', '.sh', '.bash', '.zsh', '.rb', '.go',
33
- '.rs', '.java', '.kt', '.cs', '.php', '.swift',
34
- '.scala', '.yml', '.yaml',
35
- ]);
36
26
  const IGNORED_SEGMENTS = new Set([
37
27
  '.git',
38
28
  'node_modules',
@@ -44,10 +34,13 @@ const IGNORED_SEGMENTS = new Set([
44
34
  'coverage',
45
35
  'archive',
46
36
  'generated',
37
+ 'vendor',
47
38
  'telegram_env',
48
39
  'backup-opencode-config',
49
40
  'training-data',
50
41
  ]);
42
+ const IGNORED_FILE_RX = /(?:^|\/)(?:package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?|\.DS_Store)$/i;
43
+ const DEFAULT_APPROVAL_TTL_MS = Number(process.env.ARIA_REPO_GUARD_APPROVAL_TTL_MS || 10 * 60 * 1000);
51
44
 
52
45
  type GuardStatus = 'idle' | 'watching' | 'violation' | 'error';
53
46
 
@@ -72,7 +65,16 @@ interface AlertPayload {
72
65
  relPath?: string;
73
66
  action?: string;
74
67
  detail?: string;
75
- type: 'violation' | 'blocked-delete' | 'watcher-error' | 'status';
68
+ type: 'blocked-delete' | 'watcher-error' | 'status' | 'observed-change' | 'approved-delete' | 'ignored-delete';
69
+ }
70
+
71
+ interface DeleteApproval {
72
+ repoRoot: string;
73
+ relPath: string;
74
+ approvedBy: string;
75
+ approvedAt: string;
76
+ expiresAt: string;
77
+ reason: string;
76
78
  }
77
79
 
78
80
  function writeState(update: Partial<GuardState>): GuardState {
@@ -177,19 +179,6 @@ function normalizeRel(input: string): string {
177
179
  return input.split(path.sep).join('/');
178
180
  }
179
181
 
180
- function repoHash(repoRoot: string): string {
181
- return createHash('sha256').update(repoRoot).digest('hex').slice(0, 16);
182
- }
183
-
184
- function shadowPath(repoRoot: string, relPath: string): string {
185
- return path.join(SHADOW_ROOT, repoHash(repoRoot), relPath);
186
- }
187
-
188
- function quarantinePath(repoRoot: string, relPath: string): string {
189
- const stamped = `${Date.now()}-${relPath.replace(/[\\/]/g, '__')}`;
190
- return path.join(QUARANTINE_ROOT, repoHash(repoRoot), stamped);
191
- }
192
-
193
182
  function ensureParent(filePath: string): void {
194
183
  mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o755 });
195
184
  }
@@ -231,168 +220,150 @@ function resolveRepos(config?: AriaConfig, explicit?: string[], fallbackRepoPath
231
220
  return cwd ? [cwd] : [];
232
221
  }
233
222
 
234
- function isDoctrineBound(relPath: string): boolean {
223
+ export function isRepoGuardedPath(relPath: string): boolean {
235
224
  const normalized = normalizeRel(relPath);
236
- if (!DOCTRINE_PREFIXES.some((prefix) => normalized.startsWith(prefix))) return false;
237
- if (!DOCTRINE_EXTENSIONS.has(path.extname(normalized))) return false;
238
225
  const parts = normalized.split('/');
239
226
  if (parts.some((part) => IGNORED_SEGMENTS.has(part))) return false;
240
- if (normalized.endsWith('.map')) return false;
241
- if (/package-lock\.json$|pnpm-lock\.yaml$|yarn\.lock$/i.test(normalized)) return false;
227
+ if (normalized.endsWith('.map') || IGNORED_FILE_RX.test(normalized)) return false;
242
228
  return true;
243
229
  }
244
230
 
245
- function walkDoctrineFiles(repoRoot: string): string[] {
246
- const found: string[] = [];
247
-
248
- function visit(absPath: string): void {
249
- const stat = statSync(absPath);
250
- if (stat.isDirectory()) {
251
- const base = path.basename(absPath);
252
- if (IGNORED_SEGMENTS.has(base)) return;
253
- for (const child of readdirSync(absPath)) {
254
- visit(path.join(absPath, child));
255
- }
256
- return;
257
- }
258
-
259
- const rel = path.relative(repoRoot, absPath);
260
- if (isDoctrineBound(rel)) found.push(absPath);
261
- }
262
-
263
- for (const prefix of DOCTRINE_PREFIXES) {
264
- const abs = path.join(repoRoot, prefix.slice(0, -1));
265
- if (existsSync(abs)) visit(abs);
266
- }
267
-
268
- return found;
269
- }
270
-
271
- function gateFile(repoRoot: string, relPath: string): { ok: true } | { ok: false; detail: string } {
272
- const hookPath = path.join(homedir(), '.claude', 'hooks', 'aria-repo-doctrine-gate.mjs');
231
+ function isGitTracked(repoRoot: string, relPath: string): boolean {
273
232
  try {
274
- execFileSync('node', [hookPath, '--repo', repoRoot, '--paths', relPath], {
275
- stdio: ['ignore', 'pipe', 'pipe'],
276
- encoding: 'utf8',
233
+ execFileSync('git', ['-C', repoRoot, 'ls-files', '--error-unmatch', normalizeRel(relPath)], {
234
+ stdio: ['ignore', 'ignore', 'ignore'],
277
235
  });
278
- return { ok: true };
279
- } catch (error) {
280
- const detail = error instanceof Error && 'stderr' in error
281
- ? String((error as { stderr?: string }).stderr || (error as { stdout?: string }).stdout || error.message)
282
- : String(error);
283
- return { ok: false, detail: detail.trim() || 'aria repo doctrine gate rejected the file' };
236
+ return true;
237
+ } catch {
238
+ return false;
284
239
  }
285
240
  }
286
241
 
287
- function writeShadow(repoRoot: string, relPath: string, content: string): void {
288
- const dst = shadowPath(repoRoot, relPath);
289
- ensureParent(dst);
290
- writeFileSync(dst, content, { mode: 0o600 });
291
- }
292
-
293
- function readShadowContent(repoRoot: string, relPath: string): string | null {
294
- const shadow = shadowPath(repoRoot, relPath);
295
- if (!existsSync(shadow)) return null;
242
+ function readTrackedContent(repoRoot: string, relPath: string): Buffer | null {
296
243
  try {
297
- return readFileSync(shadow, 'utf8');
244
+ return execFileSync('git', ['-C', repoRoot, 'show', `HEAD:${normalizeRel(relPath)}`], {
245
+ stdio: ['ignore', 'pipe', 'ignore'],
246
+ encoding: 'buffer',
247
+ });
298
248
  } catch {
299
249
  return null;
300
250
  }
301
251
  }
302
252
 
303
- function readTrackedContent(repoRoot: string, relPath: string): string | null {
253
+ function isGitDirectory(repoRoot: string, relPath: string): boolean {
304
254
  try {
305
- return execFileSync('git', ['-C', repoRoot, 'show', `HEAD:${normalizeRel(relPath)}`], {
255
+ const output = execFileSync('git', ['-C', repoRoot, 'ls-files', `${normalizeRel(relPath).replace(/\/+$/, '')}/`], {
306
256
  stdio: ['ignore', 'pipe', 'ignore'],
307
257
  encoding: 'utf8',
308
258
  });
259
+ return output.trim().length > 0;
309
260
  } catch {
310
- return null;
261
+ return false;
311
262
  }
312
263
  }
313
264
 
314
- function recoveryContent(repoRoot: string, relPath: string): string | null {
315
- return readShadowContent(repoRoot, relPath) ?? readTrackedContent(repoRoot, relPath);
316
- }
317
-
318
- function restorePriorContent(repoRoot: string, absPath: string, relPath: string): 'restored' | 'moved' | 'none' {
319
- const prior = recoveryContent(repoRoot, relPath);
265
+ function restoreTrackedPath(repoRoot: string, absPath: string, relPath: string): 'file-restored' | 'tree-restored' | 'none' {
266
+ const prior = readTrackedContent(repoRoot, relPath);
320
267
  if (prior !== null) {
321
268
  ensureParent(absPath);
322
269
  writeFileSync(absPath, prior, { mode: 0o644 });
323
- writeShadow(repoRoot, relPath, prior);
324
- return 'restored';
270
+ return 'file-restored';
325
271
  }
326
272
 
327
- if (!existsSync(absPath)) return 'none';
328
-
329
- const movedDst = quarantinePath(repoRoot, relPath);
330
- ensureParent(movedDst);
331
- renameSync(absPath, movedDst);
332
- return 'moved';
273
+ if (!isGitDirectory(repoRoot, relPath)) return 'none';
274
+ try {
275
+ execFileSync('git', ['-C', repoRoot, 'checkout', 'HEAD', '--', normalizeRel(relPath)], {
276
+ stdio: ['ignore', 'ignore', 'ignore'],
277
+ });
278
+ return 'tree-restored';
279
+ } catch {
280
+ return 'none';
281
+ }
333
282
  }
334
283
 
335
- function initializeBaseline(repoRoot: string): void {
336
- for (const absPath of walkDoctrineFiles(repoRoot)) {
337
- const relPath = path.relative(repoRoot, absPath);
338
- try {
339
- writeShadow(repoRoot, relPath, readFileSync(absPath, 'utf8'));
340
- } catch {}
284
+ function readApprovals(): DeleteApproval[] {
285
+ try {
286
+ const parsed = JSON.parse(readFileSync(APPROVALS_PATH, 'utf8')) as unknown;
287
+ if (!Array.isArray(parsed)) return [];
288
+ return parsed.filter((item): item is DeleteApproval => {
289
+ if (!item || typeof item !== 'object') return false;
290
+ const approval = item as Partial<DeleteApproval>;
291
+ return Boolean(approval.repoRoot && approval.relPath && approval.approvedBy && approval.expiresAt);
292
+ });
293
+ } catch {
294
+ return [];
341
295
  }
342
296
  }
343
297
 
344
- function handleViolation(repoRoot: string, absPath: string, relPath: string, detail: string): void {
345
- const fileContent = existsSync(absPath) ? readFileSync(absPath, 'utf8') : '';
346
- const quarantineDst = quarantinePath(repoRoot, relPath);
347
- ensureParent(quarantineDst);
348
- writeFileSync(quarantineDst, fileContent, { mode: 0o600 });
349
-
350
- const action = BLOCKING_MODE ? restorePriorContent(repoRoot, absPath, relPath) : 'none';
298
+ function writeApprovals(approvals: DeleteApproval[]): void {
299
+ mkdirSync(ARIA_DIR, { recursive: true, mode: 0o700 });
300
+ writeFileSync(APPROVALS_PATH, JSON.stringify(approvals, null, 2) + '\n', { mode: 0o600 });
301
+ }
302
+
303
+ function hasDeleteApproval(repoRoot: string, relPath: string): boolean {
304
+ const now = Date.now();
305
+ const approvals = readApprovals();
306
+ const active = approvals.filter((approval) => Date.parse(approval.expiresAt) > now);
307
+ if (active.length !== approvals.length) writeApprovals(active);
308
+ const normalizedRepo = path.resolve(repoRoot);
309
+ const normalizedPath = normalizeRel(relPath);
310
+ return active.some((approval) => path.resolve(approval.repoRoot) === normalizedRepo && normalizeRel(approval.relPath) === normalizedPath);
311
+ }
312
+
313
+ export function approveRepoGuardDelete(repoRoot: string, relPath: string, approvedBy = 'owner', reason = 'explicit owner approval', ttlMs = DEFAULT_APPROVAL_TTL_MS): DeleteApproval {
314
+ const approval: DeleteApproval = {
315
+ repoRoot: path.resolve(repoRoot),
316
+ relPath: normalizeRel(relPath),
317
+ approvedBy,
318
+ approvedAt: new Date().toISOString(),
319
+ expiresAt: new Date(Date.now() + ttlMs).toISOString(),
320
+ reason,
321
+ };
322
+ writeApprovals([...readApprovals(), approval]);
323
+ appendEvent({ type: 'delete-approved', ...approval });
324
+ return approval;
325
+ }
326
+
327
+ function handleDeletedGuardedPath(repoRoot: string, absPath: string, relPath: string): void {
328
+ const tracked = isGitTracked(repoRoot, relPath) || isGitDirectory(repoRoot, relPath);
329
+ if (!tracked) {
330
+ appendEvent({
331
+ type: 'ignored-delete',
332
+ repoRoot,
333
+ relPath,
334
+ detail: 'Untracked path deletion observed; nothing can be restored from git.',
335
+ });
336
+ writeState({ status: 'watching', lastEvent: `${repoRoot}:${relPath}`, lastError: null });
337
+ return;
338
+ }
351
339
 
352
- const summary = `${repoRoot}:${relPath}`;
353
- writeState({ status: 'violation', lastViolation: summary, lastEvent: summary, lastError: detail });
354
- appendEvent({
355
- type: 'violation',
356
- repoRoot,
357
- relPath,
358
- quarantineDst,
359
- blockingMode: BLOCKING_MODE,
360
- action,
361
- detail,
362
- });
363
- emitAlert({
364
- type: 'violation',
365
- title: 'Aria Repo Guard blocked a doctrine violation',
366
- message: BLOCKING_MODE
367
- ? 'The changed file was quarantined and the last known good content was restored.'
368
- : 'A doctrine violation was detected while repo guard was in non-blocking mode.',
369
- repoRoot,
370
- relPath,
371
- action,
372
- detail,
373
- });
374
- }
340
+ if (hasDeleteApproval(repoRoot, relPath)) {
341
+ appendEvent({ type: 'approved-delete', repoRoot, relPath });
342
+ writeState({ status: 'watching', lastEvent: `${repoRoot}:${relPath}`, lastError: null });
343
+ return;
344
+ }
375
345
 
376
- function handleDeletedDoctrineFile(repoRoot: string, absPath: string, relPath: string): void {
377
- const action = BLOCKING_MODE ? restorePriorContent(repoRoot, absPath, relPath) : 'none';
378
- if (action === 'none') return;
346
+ const action = BLOCKING_MODE ? restoreTrackedPath(repoRoot, absPath, relPath) : 'none';
379
347
  const summary = `${repoRoot}:${relPath}`;
380
- writeState({ status: 'violation', lastViolation: summary, lastEvent: summary, lastError: 'Doctrine-bound deletion blocked.' });
348
+ writeState({ status: 'violation', lastViolation: summary, lastEvent: summary, lastError: 'Tracked path deletion requires explicit approval.' });
381
349
  appendEvent({
382
350
  type: 'blocked-delete',
383
351
  repoRoot,
384
352
  relPath,
385
353
  blockingMode: BLOCKING_MODE,
386
354
  action,
355
+ detail: 'Tracked path deletion restored from git because no active delete approval matched this path.',
387
356
  });
388
357
  emitAlert({
389
358
  type: 'blocked-delete',
390
- title: 'Aria Repo Guard blocked a doctrine-bound deletion',
391
- message: 'A doctrine-bound file deletion was intercepted and restored.',
359
+ title: 'Aria Repo Guard blocked an unapproved tracked deletion',
360
+ message: BLOCKING_MODE
361
+ ? 'A tracked repo path was deleted without active approval and was restored from git.'
362
+ : 'A tracked repo path was deleted without active approval while repo guard was non-blocking.',
392
363
  repoRoot,
393
364
  relPath,
394
365
  action,
395
- detail: 'Doctrine-bound deletion blocked.',
366
+ detail: 'Tracked path deletion requires explicit approval.',
396
367
  });
397
368
  }
398
369
 
@@ -411,22 +382,14 @@ function findOwningRepo(repos: string[], absPath: string): string | null {
411
382
 
412
383
  function processChangedFile(repoRoot: string, absPath: string): void {
413
384
  const relPath = path.relative(repoRoot, absPath);
414
- if (!isDoctrineBound(relPath)) return;
385
+ if (!isRepoGuardedPath(relPath)) return;
415
386
  if (!existsSync(absPath)) {
416
- handleDeletedDoctrineFile(repoRoot, absPath, relPath);
387
+ handleDeletedGuardedPath(repoRoot, absPath, relPath);
417
388
  return;
418
389
  }
419
390
  if (statSync(absPath).isDirectory()) return;
420
-
421
- const gate = gateFile(repoRoot, relPath);
422
- if (gate.ok) {
423
- writeShadow(repoRoot, relPath, readFileSync(absPath, 'utf8'));
424
- const summary = `${repoRoot}:${relPath}`;
425
- writeState({ status: 'watching', lastEvent: summary, lastError: null });
426
- return;
427
- }
428
-
429
- handleViolation(repoRoot, absPath, relPath, gate.detail);
391
+ appendEvent({ type: 'observed-change', repoRoot, relPath });
392
+ writeState({ status: 'watching', lastEvent: `${repoRoot}:${relPath}`, lastError: null });
430
393
  }
431
394
 
432
395
  export async function startRepoGuardDaemon(options: RepoGuardOptions = {}): Promise<void> {
@@ -436,19 +399,16 @@ export async function startRepoGuardDaemon(options: RepoGuardOptions = {}): Prom
436
399
  throw new Error('No git repositories configured for repo guard.');
437
400
  }
438
401
 
439
- mkdirSync(SHADOW_ROOT, { recursive: true, mode: 0o700 });
440
402
  mkdirSync(QUARANTINE_ROOT, { recursive: true, mode: 0o700 });
441
- for (const repo of repos) initializeBaseline(repo);
442
403
 
443
404
  writeState({ status: 'watching', watchedRepos: repos, lastError: null });
444
405
  emitAlert({
445
406
  type: 'status',
446
407
  title: 'Aria Repo Guard watching repositories',
447
- message: `Watching ${repos.length} repo(s) for doctrine-bound changes.`,
408
+ message: `Watching ${repos.length} repo(s) for unapproved tracked deletions.`,
448
409
  });
449
410
 
450
411
  const pending = new Map<string, NodeJS.Timeout>();
451
- const restoring = new Map<string, number>();
452
412
  const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
453
413
 
454
414
  const watcher = chokidar.watch(repos, {
@@ -469,30 +429,16 @@ export async function startRepoGuardDaemon(options: RepoGuardOptions = {}): Prom
469
429
 
470
430
  const timer = setTimeout(() => {
471
431
  pending.delete(absPath);
472
- const restoredAt = restoring.get(absPath);
473
- if (restoredAt && Date.now() - restoredAt < 1500) {
474
- restoring.delete(absPath);
475
- return;
476
- }
477
432
 
478
433
  const relPath = path.relative(repoRoot, absPath);
479
- if (!isDoctrineBound(relPath)) return;
434
+ if (!isRepoGuardedPath(relPath)) return;
480
435
  if (!existsSync(absPath)) {
481
- handleDeletedDoctrineFile(repoRoot, absPath, relPath);
482
- return;
483
- }
484
-
485
- const gate = gateFile(repoRoot, relPath);
486
- if (gate.ok) {
487
- if (!statSync(absPath).isDirectory()) {
488
- writeShadow(repoRoot, relPath, readFileSync(absPath, 'utf8'));
489
- }
490
- writeState({ status: 'watching', lastEvent: `${repoRoot}:${relPath}`, lastError: null });
436
+ handleDeletedGuardedPath(repoRoot, absPath, relPath);
491
437
  return;
492
438
  }
493
439
 
494
- restoring.set(absPath, Date.now());
495
- handleViolation(repoRoot, absPath, relPath, gate.detail);
440
+ if (!statSync(absPath).isDirectory()) appendEvent({ type: 'observed-change', repoRoot, relPath });
441
+ writeState({ status: 'watching', lastEvent: `${repoRoot}:${relPath}`, lastError: null });
496
442
  }, debounceMs);
497
443
 
498
444
  pending.set(absPath, timer);
@@ -576,7 +522,6 @@ export async function installRepoGuardDaemon(config?: AriaConfig, fallbackRepoPa
576
522
  }
577
523
 
578
524
  mkdirSync(BIN_DIR, { recursive: true, mode: 0o755 });
579
- mkdirSync(SHADOW_ROOT, { recursive: true, mode: 0o700 });
580
525
  mkdirSync(QUARANTINE_ROOT, { recursive: true, mode: 0o700 });
581
526
  const envPath = writeRepoGuardEnv(repos);
582
527
  writeWrapper(path.join(BIN_DIR, 'aria-repo-guard'), ['repo-guard']);
@@ -89,11 +89,14 @@ function writeRuntimeEnv(runtimeDst: string): string {
89
89
  const envPath = path.join(runtimeDst, 'runtime.env');
90
90
  const config = loadConfig();
91
91
  const runtimeProfiles = config.runtimeProfiles || {};
92
- const defaultProvider = config.defaultProvider || config.model?.provider || 'deepseek';
93
- const chatModel = runtimeProfiles.chatModel || 'deepseek-v4-flash';
92
+ const defaultProvider = config.defaultProvider || config.model?.provider || 'xai';
93
+ const chatModel = runtimeProfiles.chatModel || 'grok-4-3';
94
94
  const deepModel = runtimeProfiles.deepModel || 'deepseek-v4-pro';
95
- const xaiFallbackModel = runtimeProfiles.xaiFallbackModel || 'grok-4-2-reasoning';
95
+ const xaiFallbackModel = runtimeProfiles.xaiFallbackModel || 'grok-4-3';
96
96
  const nimFallbackModel = runtimeProfiles.nimFallbackModel || 'qwen/qwen3.5-122b-a10b';
97
+ const localFallbackModel = runtimeProfiles.localFallbackModel || 'mlx-community/Qwen2.5-7B-Instruct-4bit';
98
+ const localFallbackBaseUrl =
99
+ runtimeProfiles.localFallbackBaseUrl || 'http://aria-lane-gateway.aria.svc.cluster.local:8089/v1/chat/completions';
97
100
  const localHarnessUrl = 'http://127.0.0.1:8790';
98
101
  const upstreamHarnessUrl = canonicalUpstreamHarnessUrl();
99
102
  const harnessFallbackUrls = [
@@ -141,8 +144,11 @@ function writeRuntimeEnv(runtimeDst: string): string {
141
144
  `ARIA_DEEP_MODEL=${deepModel}`,
142
145
  `ARIA_XAI_FALLBACK_MODEL=${xaiFallbackModel}`,
143
146
  `ARIA_NIM_FALLBACK_MODEL=${nimFallbackModel}`,
147
+ `ARIA_DEEPSEEK_FALLBACK_MODEL=${deepModel}`,
148
+ `ARIA_LOCAL_FALLBACK_MODEL=${localFallbackModel}`,
149
+ `ARIA_LOCAL_FALLBACK_BASE_URL=${localFallbackBaseUrl}`,
144
150
  'OPENAI_BASE_URL=http://127.0.0.1:4319/v1',
145
- 'ANTHROPIC_BASE_URL=http://127.0.0.1:4319/v1',
151
+ 'ANTHROPIC_BASE_URL=http://127.0.0.1:4319',
146
152
  '',
147
153
  ].join('\n'),
148
154
  { mode: 0o600 },
@@ -176,16 +182,22 @@ function ensureRuntimeProviderDefaults(): void {
176
182
  const config = loadConfig();
177
183
  saveConfig({
178
184
  ...config,
179
- defaultProvider: config.defaultProvider || config.model?.provider || 'deepseek',
185
+ defaultProvider: config.defaultProvider || config.model?.provider || 'xai',
180
186
  runtimeProfiles: {
181
- chatModel: config.runtimeProfiles?.chatModel || 'deepseek-v4-flash',
187
+ chatModel: config.runtimeProfiles?.chatModel || 'grok-4-3',
182
188
  deepModel: config.runtimeProfiles?.deepModel || 'deepseek-v4-pro',
183
- xaiFallbackModel: config.runtimeProfiles?.xaiFallbackModel || 'grok-4-2-reasoning',
189
+ xaiFallbackModel: config.runtimeProfiles?.xaiFallbackModel || 'grok-4-3',
184
190
  nimFallbackModel: config.runtimeProfiles?.nimFallbackModel || 'qwen/qwen3.5-122b-a10b',
185
191
  xaiApiKey: config.runtimeProfiles?.xaiApiKey || '',
192
+ deepseekApiKey: config.runtimeProfiles?.deepseekApiKey || '',
186
193
  nimApiKey: config.runtimeProfiles?.nimApiKey || '',
187
194
  xaiBaseUrl: config.runtimeProfiles?.xaiBaseUrl || 'https://api.x.ai/v1/chat/completions',
195
+ deepseekBaseUrl: config.runtimeProfiles?.deepseekBaseUrl || 'https://api.deepseek.com/v1/chat/completions',
188
196
  nimBaseUrl: config.runtimeProfiles?.nimBaseUrl || 'https://integrate.api.nvidia.com/v1/chat/completions',
197
+ localFallbackModel: config.runtimeProfiles?.localFallbackModel || 'mlx-community/Qwen2.5-7B-Instruct-4bit',
198
+ localFallbackBaseUrl:
199
+ config.runtimeProfiles?.localFallbackBaseUrl || 'http://aria-lane-gateway.aria.svc.cluster.local:8089/v1/chat/completions',
200
+ localFallbackApiKey: config.runtimeProfiles?.localFallbackApiKey || '',
189
201
  },
190
202
  });
191
203
  }
@@ -76,21 +76,25 @@ if [ -f "$RUNTIME_ENV" ]; then
76
76
  . "$RUNTIME_ENV"
77
77
  set +a
78
78
  fi
79
+ ARIA_SECRETS_SH="$HOME/.aria/secrets/aria-secrets.sh"
80
+ if [ -f "$ARIA_SECRETS_SH" ]; then
81
+ . "$ARIA_SECRETS_SH"
82
+ fi
79
83
  export ARIA_RUNTIME_URL="\${ARIA_RUNTIME_URL:-http://127.0.0.1:4319}"
80
84
  export OPENAI_BASE_URL="\${OPENAI_BASE_URL:-$ARIA_RUNTIME_URL/v1}"
81
- export ANTHROPIC_BASE_URL="\${ANTHROPIC_BASE_URL:-$ARIA_RUNTIME_URL/v1}"
82
- if [ -z "\${ARIA_API_KEY:-}" ]; then
85
+ export ANTHROPIC_BASE_URL="\${ANTHROPIC_BASE_URL:-$ARIA_RUNTIME_URL}"
86
+ if [ -z "\${ARIA_HARNESS_TOKEN:-}" ]; then
83
87
  if [ -f "$HOME/.aria/owner-token" ]; then
84
- export ARIA_API_KEY="$(tr -d '\\n' < "$HOME/.aria/owner-token")"
88
+ export ARIA_HARNESS_TOKEN="$(tr -d '\\n' < "$HOME/.aria/owner-token")"
85
89
  elif [ -f "$HOME/.aria/license.json" ]; then
86
- export ARIA_API_KEY="$(sed -n 's/.*"harnessToken"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' "$HOME/.aria/license.json" | head -n 1)"
87
- if [ -z "$ARIA_API_KEY" ]; then
88
- export ARIA_API_KEY="$(sed -n 's/.*"token"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' "$HOME/.aria/license.json" | head -n 1)"
90
+ export ARIA_HARNESS_TOKEN="$(sed -n 's/.*"harnessToken"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' "$HOME/.aria/license.json" | head -n 1)"
91
+ if [ -z "$ARIA_HARNESS_TOKEN" ]; then
92
+ export ARIA_HARNESS_TOKEN="$(sed -n 's/.*"token"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' "$HOME/.aria/license.json" | head -n 1)"
89
93
  fi
90
94
  fi
91
95
  fi
92
- if [ -n "\${ARIA_API_KEY:-}" ] && [ -z "\${OPENAI_API_KEY:-}" ]; then
93
- export OPENAI_API_KEY="$ARIA_API_KEY"
96
+ if [ -n "\${ARIA_HARNESS_TOKEN:-}" ] && [ -z "\${ARIA_API_KEY:-}" ]; then
97
+ export ARIA_API_KEY="$ARIA_HARNESS_TOKEN"
94
98
  fi
95
99
  `;
96
100
  }
@@ -1,5 +1,5 @@
1
- import type { AuthConfig, HarnessResponse, HarnessGateDecision } from './types.js';
2
- import { loadLicense } from './auth.js';
1
+ import type { HarnessResponse, HarnessGateDecision } from './types.js';
2
+ import { resolveHarnessToken } from './auth.js';
3
3
  import { pushCognitionLog } from './cognition-log.js';
4
4
  import * as fs from 'node:fs';
5
5
  import * as path from 'node:path';
@@ -91,7 +91,9 @@ export class HarnessClient {
91
91
  }
92
92
 
93
93
  /**
94
- * Resolve the bearer token: try loadLicense() first, fall back to env var.
94
+ * Resolve the bearer token through the shared survival resolver.
95
+ * Explicit/env tokens are persisted per client scope so later sessions do not
96
+ * silently lose auth when the process environment changes.
95
97
  * Cache the promise so we don't re-read the file on every call.
96
98
  */
97
99
  private async getToken(): Promise<string | null> {
@@ -103,25 +105,9 @@ export class HarnessClient {
103
105
  }
104
106
 
105
107
  private async resolveToken(): Promise<string | null> {
106
- // 1. Try license file
107
- try {
108
- const license = await loadLicense();
109
- if (license?.harnessToken) {
110
- this.token = license.harnessToken;
111
- return this.token;
112
- }
113
- } catch {
114
- // file not found or parse error — fall through
115
- }
116
-
117
- // 2. Fallback to env var
118
- const envToken = process.env.ARIA_HARNESS_TOKEN;
119
- if (envToken) {
120
- this.token = envToken;
121
- return this.token;
122
- }
123
-
124
- return null;
108
+ const resolved = await resolveHarnessToken({ baseUrl: this.baseUrl });
109
+ this.token = resolved.token;
110
+ return this.token;
125
111
  }
126
112
 
127
113
  /**
@@ -46,9 +46,14 @@ const MODEL_CONTEXT: Record<string, number> = {
46
46
  // DeepSeek
47
47
  'deepseek-v3': 64_000,
48
48
  'deepseek-chat': 64_000,
49
+ 'deepseek-v4-pro': 128_000,
49
50
  'deepseek-r1': 128_000,
50
51
  'deepseek-coder': 128_000,
51
52
 
53
+ // xAI
54
+ 'grok-4-3': 256_000,
55
+ 'grok-4.3': 256_000,
56
+
52
57
  // Open / local
53
58
  'qwen2.5:7b-instruct': 32_000,
54
59
  'qwen3.5-122b-a10b': 128_000,
@@ -68,6 +73,7 @@ const PROVIDER_DEFAULT: Record<ProviderName, number> = {
68
73
  google: 1_000_000,
69
74
  deepseek: 64_000,
70
75
  openrouter: 32_000, // wide range routed through; conservative chunks safer
76
+ xai: 256_000,
71
77
  ollama: 32_000, // local models vary wildly; default low
72
78
  };
73
79
 
@@ -23,7 +23,7 @@ export interface ChatResult {
23
23
  runtimeProof?: RuntimeProofSnapshot;
24
24
  }
25
25
 
26
- export type ProviderName = 'openai' | 'anthropic' | 'google' | 'deepseek' | 'openrouter' | 'ollama';
26
+ export type ProviderName = 'openai' | 'anthropic' | 'google' | 'deepseek' | 'openrouter' | 'xai' | 'ollama';
27
27
 
28
28
  export type ChatFn = (
29
29
  messages: Message[],
@@ -0,0 +1,55 @@
1
+ import type { Message, ChatOptions, ChatResult } from './types.js';
2
+
3
+ function xaiChatCompletionsUrl(): string {
4
+ const base = process.env.XAI_API_BASE || 'https://api.x.ai/v1/chat/completions';
5
+ const trimmed = base.replace(/\/+$/, '');
6
+ return trimmed.endsWith('/chat/completions') ? trimmed : `${trimmed}/chat/completions`;
7
+ }
8
+
9
+ function xaiApiModel(model: string): string {
10
+ return model === 'grok-4-3' ? 'grok-4.3' : model;
11
+ }
12
+
13
+ export async function chat(
14
+ messages: Message[],
15
+ apiKey: string,
16
+ model: string,
17
+ opts?: ChatOptions,
18
+ ): Promise<ChatResult> {
19
+ const token = apiKey || process.env.XAI_API_KEY || process.env.GROK_API_KEY || process.env.ARIA_XAI_KEY || '';
20
+ if (!token) {
21
+ throw new Error('xAI API key missing. Configure XAI_API_KEY, GROK_API_KEY, ARIA_XAI_KEY, or ~/.aria/config.json.');
22
+ }
23
+
24
+ const resp = await fetch(xaiChatCompletionsUrl(), {
25
+ method: 'POST',
26
+ headers: {
27
+ 'Content-Type': 'application/json',
28
+ Authorization: `Bearer ${token}`,
29
+ },
30
+ body: JSON.stringify({
31
+ model: xaiApiModel(model || 'grok-4-3'),
32
+ messages,
33
+ max_tokens: opts?.maxTokens ?? 4096,
34
+ stream: false,
35
+ }),
36
+ signal: opts?.signal,
37
+ });
38
+
39
+ if (!resp.ok) {
40
+ const err = await resp.text().catch(() => resp.statusText);
41
+ throw new Error(`xAI error ${resp.status}: ${err}`);
42
+ }
43
+
44
+ const data = (await resp.json()) as {
45
+ choices: { message: { content: string } }[];
46
+ usage?: { prompt_tokens: number; completion_tokens: number };
47
+ };
48
+
49
+ return {
50
+ text: data.choices[0]?.message?.content ?? '',
51
+ usage: data.usage
52
+ ? { promptTokens: data.usage.prompt_tokens, completionTokens: data.usage.completion_tokens }
53
+ : undefined,
54
+ };
55
+ }