@nathapp/nax 0.45.0 → 0.46.1
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/CHANGELOG.md +33 -0
- package/bin/nax.ts +7 -6
- package/dist/nax.js +340 -202
- package/package.json +1 -1
- package/src/acceptance/generator.ts +1 -1
- package/src/acceptance/types.ts +2 -0
- package/src/agents/acp/adapter.ts +34 -6
- package/src/agents/acp/cost.ts +5 -75
- package/src/agents/acp/index.ts +0 -2
- package/src/agents/acp/parser.ts +57 -104
- package/src/agents/acp/spawn-client.ts +13 -2
- package/src/agents/{claude.ts → claude/adapter.ts} +15 -12
- package/src/agents/{claude-complete.ts → claude/complete.ts} +3 -3
- package/src/agents/claude/cost.ts +16 -0
- package/src/agents/{claude-execution.ts → claude/execution.ts} +17 -6
- package/src/agents/claude/index.ts +3 -0
- package/src/agents/{claude-interactive.ts → claude/interactive.ts} +4 -4
- package/src/agents/{claude-plan.ts → claude/plan.ts} +12 -9
- package/src/agents/cost/calculate.ts +154 -0
- package/src/agents/cost/index.ts +10 -0
- package/src/agents/cost/parse.ts +97 -0
- package/src/agents/cost/pricing.ts +59 -0
- package/src/agents/cost/types.ts +45 -0
- package/src/agents/index.ts +6 -4
- package/src/agents/registry.ts +5 -5
- package/src/agents/{claude-decompose.ts → shared/decompose.ts} +2 -2
- package/src/agents/{model-resolution.ts → shared/model-resolution.ts} +2 -2
- package/src/agents/{types-extended.ts → shared/types-extended.ts} +4 -4
- package/src/agents/{validation.ts → shared/validation.ts} +2 -2
- package/src/agents/{version-detection.ts → shared/version-detection.ts} +3 -3
- package/src/agents/types.ts +11 -4
- package/src/cli/agents.ts +1 -1
- package/src/cli/init.ts +15 -1
- package/src/pipeline/stages/acceptance-setup.ts +1 -0
- package/src/pipeline/stages/acceptance.ts +5 -8
- package/src/pipeline/stages/regression.ts +2 -0
- package/src/pipeline/stages/verify.ts +5 -10
- package/src/precheck/checks-agents.ts +1 -1
- package/src/precheck/checks-git.ts +28 -2
- package/src/precheck/checks-warnings.ts +30 -2
- package/src/precheck/checks.ts +1 -0
- package/src/precheck/index.ts +2 -0
- package/src/utils/log-test-output.ts +25 -0
- package/src/agents/cost.ts +0 -268
- /package/src/agents/{adapters/aider.ts → aider/adapter.ts} +0 -0
- /package/src/agents/{adapters/codex.ts → codex/adapter.ts} +0 -0
- /package/src/agents/{adapters/gemini.ts → gemini/adapter.ts} +0 -0
- /package/src/agents/{adapters/opencode.ts → opencode/adapter.ts} +0 -0
package/package.json
CHANGED
|
@@ -124,7 +124,7 @@ Respond with ONLY the TypeScript test code (no markdown code fences, no explanat
|
|
|
124
124
|
2,
|
|
125
125
|
);
|
|
126
126
|
|
|
127
|
-
await _generatorPRDDeps.writeFile(join(options.
|
|
127
|
+
await _generatorPRDDeps.writeFile(join(options.featureDir, "acceptance-refined.json"), refinedJsonContent);
|
|
128
128
|
|
|
129
129
|
return { testCode, criteria };
|
|
130
130
|
}
|
package/src/acceptance/types.ts
CHANGED
|
@@ -80,6 +80,8 @@ export interface GenerateFromPRDOptions {
|
|
|
80
80
|
featureName: string;
|
|
81
81
|
/** Working directory for context scanning */
|
|
82
82
|
workdir: string;
|
|
83
|
+
/** Feature directory where acceptance-refined.json is written */
|
|
84
|
+
featureDir: string;
|
|
83
85
|
/** Codebase context (file tree, dependencies, test patterns) */
|
|
84
86
|
codebaseContext: string;
|
|
85
87
|
/** Model tier to use for test generation */
|
|
@@ -15,7 +15,7 @@ import { createHash } from "node:crypto";
|
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import { resolvePermissions } from "../../config/permissions";
|
|
17
17
|
import { getSafeLogger } from "../../logger";
|
|
18
|
-
import { buildDecomposePrompt, parseDecomposeOutput } from "../
|
|
18
|
+
import { buildDecomposePrompt, parseDecomposeOutput } from "../shared/decompose";
|
|
19
19
|
import { createSpawnAcpClient } from "./spawn-client";
|
|
20
20
|
|
|
21
21
|
import type {
|
|
@@ -80,7 +80,14 @@ const DEFAULT_ENTRY: AgentRegistryEntry = {
|
|
|
80
80
|
export interface AcpSessionResponse {
|
|
81
81
|
messages: Array<{ role: string; content: string }>;
|
|
82
82
|
stopReason: string;
|
|
83
|
-
cumulative_token_usage?: {
|
|
83
|
+
cumulative_token_usage?: {
|
|
84
|
+
input_tokens: number;
|
|
85
|
+
output_tokens: number;
|
|
86
|
+
cache_read_input_tokens?: number;
|
|
87
|
+
cache_creation_input_tokens?: number;
|
|
88
|
+
};
|
|
89
|
+
/** Exact cost in USD from acpx usage_update event. Preferred over token-based estimation. */
|
|
90
|
+
exactCostUsd?: number;
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
export interface AcpSession {
|
|
@@ -555,7 +562,13 @@ export class AcpAgentAdapter implements AgentAdapter {
|
|
|
555
562
|
// Tracks whether the run completed successfully — used by finally to decide
|
|
556
563
|
// whether to close the session (success) or keep it open for retry (failure).
|
|
557
564
|
const runState = { succeeded: false };
|
|
558
|
-
const totalTokenUsage = {
|
|
565
|
+
const totalTokenUsage = {
|
|
566
|
+
input_tokens: 0,
|
|
567
|
+
output_tokens: 0,
|
|
568
|
+
cache_read_input_tokens: 0,
|
|
569
|
+
cache_creation_input_tokens: 0,
|
|
570
|
+
};
|
|
571
|
+
let totalExactCostUsd: number | undefined;
|
|
559
572
|
|
|
560
573
|
try {
|
|
561
574
|
// 5. Multi-turn loop
|
|
@@ -577,10 +590,16 @@ export class AcpAgentAdapter implements AgentAdapter {
|
|
|
577
590
|
lastResponse = turnResult.response;
|
|
578
591
|
if (!lastResponse) break;
|
|
579
592
|
|
|
580
|
-
// Accumulate token usage
|
|
593
|
+
// Accumulate token usage and exact cost
|
|
581
594
|
if (lastResponse.cumulative_token_usage) {
|
|
582
595
|
totalTokenUsage.input_tokens += lastResponse.cumulative_token_usage.input_tokens ?? 0;
|
|
583
596
|
totalTokenUsage.output_tokens += lastResponse.cumulative_token_usage.output_tokens ?? 0;
|
|
597
|
+
totalTokenUsage.cache_read_input_tokens += lastResponse.cumulative_token_usage.cache_read_input_tokens ?? 0;
|
|
598
|
+
totalTokenUsage.cache_creation_input_tokens +=
|
|
599
|
+
lastResponse.cumulative_token_usage.cache_creation_input_tokens ?? 0;
|
|
600
|
+
}
|
|
601
|
+
if (lastResponse.exactCostUsd !== undefined) {
|
|
602
|
+
totalExactCostUsd = (totalExactCostUsd ?? 0) + lastResponse.exactCostUsd;
|
|
584
603
|
}
|
|
585
604
|
|
|
586
605
|
// Check for agent question → route to interaction bridge
|
|
@@ -643,10 +662,12 @@ export class AcpAgentAdapter implements AgentAdapter {
|
|
|
643
662
|
const success = lastResponse?.stopReason === "end_turn";
|
|
644
663
|
const output = extractOutput(lastResponse);
|
|
645
664
|
|
|
665
|
+
// Prefer exact cost from acpx usage_update; fall back to token-based estimation
|
|
646
666
|
const estimatedCost =
|
|
647
|
-
|
|
667
|
+
totalExactCostUsd ??
|
|
668
|
+
(totalTokenUsage.input_tokens > 0 || totalTokenUsage.output_tokens > 0
|
|
648
669
|
? estimateCostFromTokenUsage(totalTokenUsage, options.modelDef.model)
|
|
649
|
-
: 0;
|
|
670
|
+
: 0);
|
|
650
671
|
|
|
651
672
|
return {
|
|
652
673
|
success,
|
|
@@ -719,6 +740,13 @@ export class AcpAgentAdapter implements AgentAdapter {
|
|
|
719
740
|
throw new CompleteError("complete() returned empty output");
|
|
720
741
|
}
|
|
721
742
|
|
|
743
|
+
if (response.exactCostUsd !== undefined) {
|
|
744
|
+
getSafeLogger()?.info("acp-adapter", "complete() cost", {
|
|
745
|
+
costUsd: response.exactCostUsd,
|
|
746
|
+
model,
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
722
750
|
return unwrapped;
|
|
723
751
|
} catch (err) {
|
|
724
752
|
const error = err instanceof Error ? err : new Error(String(err));
|
package/src/agents/acp/cost.ts
CHANGED
|
@@ -1,79 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ACP cost estimation from
|
|
2
|
+
* ACP cost estimation — re-exports from the shared src/agents/cost/ module.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Kept for zero-breakage backward compatibility.
|
|
5
|
+
* Import directly from src/agents/cost for new code.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*/
|
|
10
|
-
export interface SessionTokenUsage {
|
|
11
|
-
input_tokens: number;
|
|
12
|
-
output_tokens: number;
|
|
13
|
-
/** Cache read tokens — billed at a reduced rate */
|
|
14
|
-
cache_read_input_tokens?: number;
|
|
15
|
-
/** Cache creation tokens — billed at a higher creation rate */
|
|
16
|
-
cache_creation_input_tokens?: number;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Per-model pricing in $/1M tokens: { input, output }
|
|
21
|
-
*/
|
|
22
|
-
const MODEL_PRICING: Record<string, { input: number; output: number; cacheRead?: number; cacheCreation?: number }> = {
|
|
23
|
-
// Anthropic Claude models
|
|
24
|
-
"claude-sonnet-4": { input: 3, output: 15 },
|
|
25
|
-
"claude-sonnet-4-5": { input: 3, output: 15 },
|
|
26
|
-
"claude-haiku": { input: 0.8, output: 4.0, cacheRead: 0.1, cacheCreation: 1.0 },
|
|
27
|
-
"claude-haiku-4-5": { input: 0.8, output: 4.0, cacheRead: 0.1, cacheCreation: 1.0 },
|
|
28
|
-
"claude-opus": { input: 15, output: 75 },
|
|
29
|
-
"claude-opus-4": { input: 15, output: 75 },
|
|
30
|
-
|
|
31
|
-
// OpenAI models
|
|
32
|
-
"gpt-4.1": { input: 10, output: 30 },
|
|
33
|
-
"gpt-4": { input: 30, output: 60 },
|
|
34
|
-
"gpt-3.5-turbo": { input: 0.5, output: 1.5 },
|
|
35
|
-
|
|
36
|
-
// Google Gemini
|
|
37
|
-
"gemini-2.5-pro": { input: 0.075, output: 0.3 },
|
|
38
|
-
"gemini-2-pro": { input: 0.075, output: 0.3 },
|
|
39
|
-
|
|
40
|
-
// OpenAI Codex
|
|
41
|
-
codex: { input: 0.02, output: 0.06 },
|
|
42
|
-
"code-davinci-002": { input: 0.02, output: 0.06 },
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Calculate USD cost from ACP session token counts using per-model pricing.
|
|
47
|
-
*
|
|
48
|
-
* @param usage - Token counts from cumulative_token_usage
|
|
49
|
-
* @param model - Model identifier (e.g., 'claude-sonnet-4', 'claude-haiku-4-5')
|
|
50
|
-
* @returns Estimated cost in USD
|
|
51
|
-
*/
|
|
52
|
-
export function estimateCostFromTokenUsage(usage: SessionTokenUsage, model: string): number {
|
|
53
|
-
const pricing = MODEL_PRICING[model];
|
|
54
|
-
|
|
55
|
-
if (!pricing) {
|
|
56
|
-
// Fallback: use average rate for unknown models
|
|
57
|
-
// Average of known rates: ~$5/1M tokens combined
|
|
58
|
-
const fallbackInputRate = 3 / 1_000_000;
|
|
59
|
-
const fallbackOutputRate = 15 / 1_000_000;
|
|
60
|
-
const inputCost = (usage.input_tokens ?? 0) * fallbackInputRate;
|
|
61
|
-
const outputCost = (usage.output_tokens ?? 0) * fallbackOutputRate;
|
|
62
|
-
const cacheReadCost = (usage.cache_read_input_tokens ?? 0) * (0.5 / 1_000_000);
|
|
63
|
-
const cacheCreationCost = (usage.cache_creation_input_tokens ?? 0) * (2 / 1_000_000);
|
|
64
|
-
return inputCost + outputCost + cacheReadCost + cacheCreationCost;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Convert $/1M rates to $/token
|
|
68
|
-
const inputRate = pricing.input / 1_000_000;
|
|
69
|
-
const outputRate = pricing.output / 1_000_000;
|
|
70
|
-
const cacheReadRate = (pricing.cacheRead ?? pricing.input * 0.1) / 1_000_000;
|
|
71
|
-
const cacheCreationRate = (pricing.cacheCreation ?? pricing.input * 0.33) / 1_000_000;
|
|
72
|
-
|
|
73
|
-
const inputCost = (usage.input_tokens ?? 0) * inputRate;
|
|
74
|
-
const outputCost = (usage.output_tokens ?? 0) * outputRate;
|
|
75
|
-
const cacheReadCost = (usage.cache_read_input_tokens ?? 0) * cacheReadRate;
|
|
76
|
-
const cacheCreationCost = (usage.cache_creation_input_tokens ?? 0) * cacheCreationRate;
|
|
77
|
-
|
|
78
|
-
return inputCost + outputCost + cacheReadCost + cacheCreationCost;
|
|
79
|
-
}
|
|
8
|
+
export type { SessionTokenUsage } from "../cost";
|
|
9
|
+
export { estimateCostFromTokenUsage } from "../cost";
|
package/src/agents/acp/index.ts
CHANGED
|
@@ -4,6 +4,4 @@
|
|
|
4
4
|
|
|
5
5
|
export { AcpAgentAdapter, _acpAdapterDeps } from "./adapter";
|
|
6
6
|
export { createSpawnAcpClient } from "./spawn-client";
|
|
7
|
-
export { estimateCostFromTokenUsage } from "./cost";
|
|
8
|
-
export type { SessionTokenUsage } from "./cost";
|
|
9
7
|
export type { AgentRegistryEntry } from "./types";
|
package/src/agents/acp/parser.ts
CHANGED
|
@@ -2,11 +2,9 @@
|
|
|
2
2
|
* ACP adapter — NDJSON and JSON-RPC output parsing helpers.
|
|
3
3
|
*
|
|
4
4
|
* Extracted from adapter.ts to keep that file within the 800-line limit.
|
|
5
|
-
* Used
|
|
5
|
+
* Used by SpawnAcpSession.prompt() to parse acpx stdout.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { AgentRunOptions } from "../types";
|
|
9
|
-
|
|
10
8
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
9
|
// Types
|
|
12
10
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -15,131 +13,86 @@ import type { AgentRunOptions } from "../types";
|
|
|
15
13
|
export interface AcpxTokenUsage {
|
|
16
14
|
input_tokens: number;
|
|
17
15
|
output_tokens: number;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
/** JSON-RPC message from acpx --format json --json-strict */
|
|
21
|
-
interface JsonRpcMessage {
|
|
22
|
-
jsonrpc: "2.0";
|
|
23
|
-
method?: string;
|
|
24
|
-
params?: {
|
|
25
|
-
sessionId: string;
|
|
26
|
-
update?: {
|
|
27
|
-
sessionUpdate: string;
|
|
28
|
-
content?: { type: string; text?: string };
|
|
29
|
-
used?: number;
|
|
30
|
-
size?: number;
|
|
31
|
-
cost?: { amount: number; currency: string };
|
|
32
|
-
};
|
|
33
|
-
};
|
|
34
|
-
id?: number | string;
|
|
35
|
-
result?: unknown;
|
|
36
|
-
error?: { code: number; message: string };
|
|
16
|
+
cache_read_input_tokens?: number;
|
|
17
|
+
cache_creation_input_tokens?: number;
|
|
37
18
|
}
|
|
38
19
|
|
|
39
20
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
40
|
-
//
|
|
21
|
+
// parseAcpxJsonOutput
|
|
41
22
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
42
23
|
|
|
43
24
|
/**
|
|
44
|
-
*
|
|
25
|
+
* Parse acpx NDJSON output for assistant text, token usage, and exact cost.
|
|
26
|
+
*
|
|
27
|
+
* Handles the JSON-RPC envelope format emitted by acpx:
|
|
28
|
+
* - session/update agent_message_chunk → text accumulation
|
|
29
|
+
* - session/update usage_update → exact cost (cost.amount) + context size
|
|
30
|
+
* - id/result → token breakdown (inputTokens, outputTokens, cachedWriteTokens, cachedReadTokens)
|
|
31
|
+
*
|
|
32
|
+
* Also handles legacy flat NDJSON format for backward compatibility.
|
|
45
33
|
*/
|
|
46
|
-
export
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
34
|
+
export function parseAcpxJsonOutput(rawOutput: string): {
|
|
35
|
+
text: string;
|
|
36
|
+
tokenUsage?: AcpxTokenUsage;
|
|
37
|
+
exactCostUsd?: number;
|
|
38
|
+
stopReason?: string;
|
|
39
|
+
error?: string;
|
|
40
|
+
} {
|
|
41
|
+
const lines = rawOutput.split("\n").filter((l) => l.trim());
|
|
42
|
+
let text = "";
|
|
52
43
|
let tokenUsage: AcpxTokenUsage | undefined;
|
|
53
|
-
|
|
54
|
-
let
|
|
55
|
-
|
|
56
|
-
const reader = stdout.getReader();
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
while (true) {
|
|
60
|
-
const { done, value } = await reader.read();
|
|
61
|
-
if (done) break;
|
|
62
|
-
|
|
63
|
-
buffer += decoder.decode(value, { stream: true });
|
|
64
|
-
const lines = buffer.split("\n");
|
|
65
|
-
buffer = lines.pop() ?? "";
|
|
66
|
-
|
|
67
|
-
for (const line of lines) {
|
|
68
|
-
if (!line.trim()) continue;
|
|
44
|
+
let exactCostUsd: number | undefined;
|
|
45
|
+
let stopReason: string | undefined;
|
|
46
|
+
let error: string | undefined;
|
|
69
47
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
} catch {
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
48
|
+
for (const line of lines) {
|
|
49
|
+
try {
|
|
50
|
+
const event = JSON.parse(line);
|
|
76
51
|
|
|
77
|
-
|
|
78
|
-
|
|
52
|
+
// ── JSON-RPC envelope format (acpx v0.3+) ──────────────────────────────
|
|
53
|
+
if (event.jsonrpc === "2.0") {
|
|
54
|
+
// session/update events
|
|
55
|
+
if (event.method === "session/update" && event.params?.update) {
|
|
56
|
+
const update = event.params.update;
|
|
79
57
|
|
|
58
|
+
// Text chunks
|
|
80
59
|
if (
|
|
81
60
|
update.sessionUpdate === "agent_message_chunk" &&
|
|
82
61
|
update.content?.type === "text" &&
|
|
83
62
|
update.content.text
|
|
84
63
|
) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (bridge?.detectQuestion && bridge.onQuestionDetected) {
|
|
88
|
-
const isQuestion = await bridge.detectQuestion(accumulatedText);
|
|
89
|
-
if (isQuestion) {
|
|
90
|
-
const response = await bridge.onQuestionDetected(accumulatedText);
|
|
91
|
-
accumulatedText += `\n\n[Human response: ${response}]`;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
64
|
+
text += update.content.text;
|
|
94
65
|
}
|
|
95
66
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
input_tokens: Math.floor(total * 0.3),
|
|
100
|
-
output_tokens: Math.floor(total * 0.7),
|
|
101
|
-
};
|
|
67
|
+
// Exact cost from usage_update
|
|
68
|
+
if (update.sessionUpdate === "usage_update" && typeof update.cost?.amount === "number") {
|
|
69
|
+
exactCostUsd = update.cost.amount;
|
|
102
70
|
}
|
|
103
71
|
}
|
|
104
72
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
accumulatedText += result;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
} finally {
|
|
114
|
-
reader.releaseLock();
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return { text: accumulatedText.trim(), tokenUsage };
|
|
118
|
-
}
|
|
73
|
+
// Final result with token breakdown (camelCase from acpx)
|
|
74
|
+
if (event.id !== undefined && event.result && typeof event.result === "object") {
|
|
75
|
+
const result = event.result as Record<string, unknown>;
|
|
119
76
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
77
|
+
if (result.stopReason) stopReason = result.stopReason as string;
|
|
78
|
+
if (result.stop_reason) stopReason = result.stop_reason as string;
|
|
123
79
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
let tokenUsage: AcpxTokenUsage | undefined;
|
|
136
|
-
let stopReason: string | undefined;
|
|
137
|
-
let error: string | undefined;
|
|
80
|
+
if (result.usage && typeof result.usage === "object") {
|
|
81
|
+
const u = result.usage as Record<string, unknown>;
|
|
82
|
+
tokenUsage = {
|
|
83
|
+
input_tokens: (u.inputTokens as number) ?? (u.input_tokens as number) ?? 0,
|
|
84
|
+
output_tokens: (u.outputTokens as number) ?? (u.output_tokens as number) ?? 0,
|
|
85
|
+
cache_read_input_tokens: (u.cachedReadTokens as number) ?? (u.cache_read_input_tokens as number) ?? 0,
|
|
86
|
+
cache_creation_input_tokens:
|
|
87
|
+
(u.cachedWriteTokens as number) ?? (u.cache_creation_input_tokens as number) ?? 0,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
138
91
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const event = JSON.parse(line);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
142
94
|
|
|
95
|
+
// ── Legacy flat NDJSON format ───────────────────────────────────────────
|
|
143
96
|
if (event.content && typeof event.content === "string") text += event.content;
|
|
144
97
|
if (event.text && typeof event.text === "string") text += event.text;
|
|
145
98
|
if (event.result && typeof event.result === "string") text = event.result;
|
|
@@ -162,5 +115,5 @@ export function parseAcpxJsonOutput(rawOutput: string): {
|
|
|
162
115
|
}
|
|
163
116
|
}
|
|
164
117
|
|
|
165
|
-
return { text: text.trim(), tokenUsage, stopReason, error };
|
|
118
|
+
return { text: text.trim(), tokenUsage, exactCostUsd, stopReason, error };
|
|
166
119
|
}
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* acpx <agent> cancel → session.cancelActivePrompt()
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { isAbsolute } from "node:path";
|
|
15
17
|
import type { PidRegistry } from "../../execution/pid-registry";
|
|
16
18
|
import { getSafeLogger } from "../../logger";
|
|
17
19
|
import type { AcpClient, AcpSession, AcpSessionResponse } from "./adapter";
|
|
@@ -60,11 +62,19 @@ export const _spawnClientDeps = {
|
|
|
60
62
|
function buildAllowedEnv(extraEnv?: Record<string, string | undefined>): Record<string, string | undefined> {
|
|
61
63
|
const allowed: Record<string, string | undefined> = {};
|
|
62
64
|
|
|
63
|
-
const essentialVars = ["PATH", "
|
|
65
|
+
const essentialVars = ["PATH", "TMPDIR", "NODE_ENV", "USER", "LOGNAME"];
|
|
64
66
|
for (const varName of essentialVars) {
|
|
65
67
|
if (process.env[varName]) allowed[varName] = process.env[varName];
|
|
66
68
|
}
|
|
67
69
|
|
|
70
|
+
// Sanitize HOME — must be absolute path. Unexpanded "~" causes literal ~/dir in cwd.
|
|
71
|
+
const rawHome = process.env.HOME ?? "";
|
|
72
|
+
const safeHome = rawHome && isAbsolute(rawHome) ? rawHome : homedir();
|
|
73
|
+
if (rawHome !== safeHome) {
|
|
74
|
+
getSafeLogger()?.warn("env", `HOME env is not absolute ("${rawHome}"), falling back to os.homedir(): ${safeHome}`);
|
|
75
|
+
}
|
|
76
|
+
allowed.HOME = safeHome;
|
|
77
|
+
|
|
68
78
|
const apiKeyVars = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY", "GOOGLE_API_KEY", "CLAUDE_API_KEY"];
|
|
69
79
|
for (const varName of apiKeyVars) {
|
|
70
80
|
if (process.env[varName]) allowed[varName] = process.env[varName];
|
|
@@ -180,8 +190,9 @@ class SpawnAcpSession implements AcpSession {
|
|
|
180
190
|
const parsed = parseAcpxJsonOutput(stdout);
|
|
181
191
|
return {
|
|
182
192
|
messages: [{ role: "assistant", content: parsed.text || "" }],
|
|
183
|
-
stopReason: "end_turn",
|
|
193
|
+
stopReason: parsed.stopReason ?? "end_turn",
|
|
184
194
|
cumulative_token_usage: parsed.tokenUsage,
|
|
195
|
+
exactCostUsd: parsed.exactCostUsd,
|
|
185
196
|
};
|
|
186
197
|
} catch (err) {
|
|
187
198
|
getSafeLogger()?.warn("acp-adapter", "Failed to parse session prompt response", {
|
|
@@ -4,15 +4,11 @@
|
|
|
4
4
|
* Main adapter class coordinating execution, completion, decomposition, and interactive modes.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { resolvePermissions } from "
|
|
8
|
-
import { PidRegistry } from "
|
|
9
|
-
import { withProcessTimeout } from "
|
|
10
|
-
import { getLogger } from "
|
|
11
|
-
import {
|
|
12
|
-
import { buildDecomposePrompt, parseDecomposeOutput } from "./claude-decompose";
|
|
13
|
-
import { _runOnceDeps, buildAllowedEnv, buildCommand, executeOnce } from "./claude-execution";
|
|
14
|
-
import { runInteractiveMode } from "./claude-interactive";
|
|
15
|
-
import { runPlan } from "./claude-plan";
|
|
7
|
+
import { resolvePermissions } from "../../config/permissions";
|
|
8
|
+
import { PidRegistry } from "../../execution/pid-registry";
|
|
9
|
+
import { withProcessTimeout } from "../../execution/timeout-handler";
|
|
10
|
+
import { getLogger } from "../../logger";
|
|
11
|
+
import { buildDecomposePrompt, parseDecomposeOutput } from "../shared/decompose";
|
|
16
12
|
import type {
|
|
17
13
|
AgentAdapter,
|
|
18
14
|
AgentCapabilities,
|
|
@@ -25,7 +21,11 @@ import type {
|
|
|
25
21
|
PlanOptions,
|
|
26
22
|
PlanResult,
|
|
27
23
|
PtyHandle,
|
|
28
|
-
} from "
|
|
24
|
+
} from "../types";
|
|
25
|
+
import { _completeDeps, executeComplete } from "./complete";
|
|
26
|
+
import { _runOnceDeps, buildAllowedEnv, buildCommand, executeOnce } from "./execution";
|
|
27
|
+
import { runInteractiveMode } from "./interactive";
|
|
28
|
+
import { runPlan } from "./plan";
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Injectable dependencies for decompose() — allows tests to intercept
|
|
@@ -174,7 +174,7 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
async decompose(options: DecomposeOptions): Promise<DecomposeResult> {
|
|
177
|
-
const { resolveBalancedModelDef } = await import("
|
|
177
|
+
const { resolveBalancedModelDef } = await import("../shared/model-resolution");
|
|
178
178
|
|
|
179
179
|
const prompt = buildDecomposePrompt(options);
|
|
180
180
|
|
|
@@ -186,7 +186,10 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
186
186
|
modelDef = resolveBalancedModelDef(options.config);
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
const { skipPermissions } = resolvePermissions(
|
|
189
|
+
const { skipPermissions } = resolvePermissions(
|
|
190
|
+
options.config as import("../../config").NaxConfig | undefined,
|
|
191
|
+
"run",
|
|
192
|
+
);
|
|
190
193
|
const cmd = [this.binary, "--model", modelDef.model, "-p", prompt];
|
|
191
194
|
if (skipPermissions) {
|
|
192
195
|
cmd.splice(cmd.length - 2, 0, "--dangerously-skip-permissions");
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* Standalone completion endpoint for simple prompts.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { resolvePermissions } from "
|
|
8
|
-
import type { CompleteOptions } from "
|
|
9
|
-
import { CompleteError } from "
|
|
7
|
+
import { resolvePermissions } from "../../config/permissions";
|
|
8
|
+
import type { CompleteOptions } from "../types";
|
|
9
|
+
import { CompleteError } from "../types";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Injectable dependencies for complete() — allows tests to intercept
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cost Tracking — re-exports from the shared src/agents/cost/ module.
|
|
3
|
+
*
|
|
4
|
+
* Kept for zero-breakage backward compatibility.
|
|
5
|
+
* Import directly from src/agents/cost for new code.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type { ModelCostRates, TokenUsage, CostEstimate, TokenUsageWithConfidence } from "../cost";
|
|
9
|
+
export {
|
|
10
|
+
COST_RATES,
|
|
11
|
+
parseTokenUsage,
|
|
12
|
+
estimateCost,
|
|
13
|
+
estimateCostFromOutput,
|
|
14
|
+
estimateCostByDuration,
|
|
15
|
+
formatCostWithConfidence,
|
|
16
|
+
} from "../cost";
|
|
@@ -4,12 +4,14 @@
|
|
|
4
4
|
* Handles building commands, preparing environment, and process execution.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { isAbsolute } from "node:path";
|
|
9
|
+
import { resolvePermissions } from "../../config/permissions";
|
|
10
|
+
import type { PidRegistry } from "../../execution/pid-registry";
|
|
11
|
+
import { withProcessTimeout } from "../../execution/timeout-handler";
|
|
12
|
+
import { getLogger } from "../../logger";
|
|
13
|
+
import type { AgentResult, AgentRunOptions } from "../types";
|
|
11
14
|
import { estimateCostByDuration, estimateCostFromOutput } from "./cost";
|
|
12
|
-
import type { AgentResult, AgentRunOptions } from "./types";
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* Maximum characters to capture from agent stdout.
|
|
@@ -65,13 +67,22 @@ export function buildCommand(binary: string, options: AgentRunOptions): string[]
|
|
|
65
67
|
export function buildAllowedEnv(options: AgentRunOptions): Record<string, string | undefined> {
|
|
66
68
|
const allowed: Record<string, string | undefined> = {};
|
|
67
69
|
|
|
68
|
-
const essentialVars = ["PATH", "
|
|
70
|
+
const essentialVars = ["PATH", "TMPDIR", "NODE_ENV", "USER", "LOGNAME"];
|
|
69
71
|
for (const varName of essentialVars) {
|
|
70
72
|
if (process.env[varName]) {
|
|
71
73
|
allowed[varName] = process.env[varName];
|
|
72
74
|
}
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
// Sanitize HOME — must be absolute path. Unexpanded "~" causes literal ~/dir in cwd.
|
|
78
|
+
const rawHome = process.env.HOME ?? "";
|
|
79
|
+
const safeHome = rawHome && isAbsolute(rawHome) ? rawHome : homedir();
|
|
80
|
+
if (rawHome !== safeHome) {
|
|
81
|
+
const logger = getLogger();
|
|
82
|
+
logger.warn("env", `HOME env is not absolute ("${rawHome}"), falling back to os.homedir(): ${safeHome}`);
|
|
83
|
+
}
|
|
84
|
+
allowed.HOME = safeHome;
|
|
85
|
+
|
|
75
86
|
const apiKeyVars = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"];
|
|
76
87
|
for (const varName of apiKeyVars) {
|
|
77
88
|
if (process.env[varName]) {
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* Handles terminal UI interactions with the Claude agent.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { PidRegistry } from "
|
|
8
|
-
import { getLogger } from "
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
7
|
+
import type { PidRegistry } from "../../execution/pid-registry";
|
|
8
|
+
import { getLogger } from "../../logger";
|
|
9
|
+
import type { AgentRunOptions, InteractiveRunOptions, PtyHandle } from "../types";
|
|
10
|
+
import { buildAllowedEnv } from "./execution";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Run Claude agent in interactive (TTY) mode for TUI output.
|
|
@@ -7,13 +7,13 @@ import { join } from "node:path";
|
|
|
7
7
|
* Extracted from claude.ts: plan(), buildPlanCommand()
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { resolvePermissions } from "
|
|
11
|
-
import type { PidRegistry } from "
|
|
12
|
-
import { withProcessTimeout } from "
|
|
13
|
-
import { getLogger } from "
|
|
14
|
-
import { resolveBalancedModelDef } from "
|
|
15
|
-
import type {
|
|
16
|
-
import type {
|
|
10
|
+
import { resolvePermissions } from "../../config/permissions";
|
|
11
|
+
import type { PidRegistry } from "../../execution/pid-registry";
|
|
12
|
+
import { withProcessTimeout } from "../../execution/timeout-handler";
|
|
13
|
+
import { getLogger } from "../../logger";
|
|
14
|
+
import { resolveBalancedModelDef } from "../shared/model-resolution";
|
|
15
|
+
import type { PlanOptions, PlanResult } from "../shared/types-extended";
|
|
16
|
+
import type { AgentRunOptions } from "../types";
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Build the CLI command for plan mode.
|
|
@@ -32,7 +32,10 @@ export function buildPlanCommand(binary: string, options: PlanOptions): string[]
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
// Resolve permission mode from config
|
|
35
|
-
const { skipPermissions } = resolvePermissions(
|
|
35
|
+
const { skipPermissions } = resolvePermissions(
|
|
36
|
+
options.config as import("../../config").NaxConfig | undefined,
|
|
37
|
+
"plan",
|
|
38
|
+
);
|
|
36
39
|
if (skipPermissions) {
|
|
37
40
|
cmd.push("--dangerously-skip-permissions");
|
|
38
41
|
}
|
|
@@ -75,7 +78,7 @@ export async function runPlan(
|
|
|
75
78
|
pidRegistry: PidRegistry,
|
|
76
79
|
buildAllowedEnv: (options: AgentRunOptions) => Record<string, string | undefined>,
|
|
77
80
|
): Promise<PlanResult> {
|
|
78
|
-
const { resolveBalancedModelDef } = await import("
|
|
81
|
+
const { resolveBalancedModelDef } = await import("../shared/model-resolution");
|
|
79
82
|
|
|
80
83
|
const cmd = buildPlanCommand(binary, options);
|
|
81
84
|
|