@sna-sdk/core 0.9.9 → 0.9.11

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.
@@ -1,9 +1,40 @@
1
1
  import { AgentProvider, SpawnOptions, AgentProcess } from './types.js';
2
2
 
3
+ /**
4
+ * Parse `command -v claude` output to extract the executable path.
5
+ * Handles: direct paths, alias with/without quotes, bare command names.
6
+ * @internal Exported for testing only.
7
+ */
8
+ declare function parseCommandVOutput(raw: string): string;
9
+ /**
10
+ * Validate a Claude CLI path by running `<path> --version`.
11
+ * Adds the binary's directory to PATH so shebang resolution works (nvm/fnm).
12
+ */
13
+ declare function validateClaudePath(claudePath: string): {
14
+ ok: boolean;
15
+ version?: string;
16
+ };
17
+ /**
18
+ * Save a validated Claude path to cache for faster startup next time.
19
+ */
20
+ declare function cacheClaudePath(claudePath: string, cacheDir?: string): void;
21
+ interface ResolveResult {
22
+ path: string;
23
+ version?: string;
24
+ source: "env" | "cache" | "static" | "shell" | "fallback";
25
+ }
26
+ /**
27
+ * Resolve Claude CLI path. Tries: env override → cache → static paths → shell detection.
28
+ * All candidates are validated with `--version` before returning.
29
+ * Consumer apps should call this and handle the `fallback` source (= not found).
30
+ */
31
+ declare function resolveClaudeCli(opts?: {
32
+ cacheDir?: string;
33
+ }): ResolveResult;
3
34
  declare class ClaudeCodeProvider implements AgentProvider {
4
35
  readonly name = "claude-code";
5
36
  isAvailable(): Promise<boolean>;
6
37
  spawn(options: SpawnOptions): AgentProcess;
7
38
  }
8
39
 
9
- export { ClaudeCodeProvider };
40
+ export { ClaudeCodeProvider, type ResolveResult, cacheClaudePath, parseCommandVOutput, resolveClaudeCli, validateClaudePath };
@@ -6,38 +6,80 @@ import { fileURLToPath } from "url";
6
6
  import { writeHistoryJsonl, buildRecalledConversation } from "./cc-history-adapter.js";
7
7
  import { logger } from "../../lib/logger.js";
8
8
  const SHELL = process.env.SHELL || "/bin/zsh";
9
- function resolveClaudePath(cwd) {
10
- if (process.env.SNA_CLAUDE_COMMAND) return process.env.SNA_CLAUDE_COMMAND;
11
- const cached = path.join(cwd, ".sna/claude-path");
12
- if (fs.existsSync(cached)) {
13
- const p = fs.readFileSync(cached, "utf8").trim();
14
- if (p) {
15
- try {
16
- execSync(`test -x "${p}"`, { stdio: "pipe" });
17
- return p;
18
- } catch {
19
- }
9
+ function parseCommandVOutput(raw) {
10
+ const trimmed = raw.trim();
11
+ if (!trimmed) return "claude";
12
+ const aliasMatch = trimmed.match(/=\s*['"]?([^'"]+?)['"]?\s*$/);
13
+ if (aliasMatch) return aliasMatch[1];
14
+ const pathMatch = trimmed.match(/^(\/\S+)/m);
15
+ if (pathMatch) return pathMatch[1];
16
+ return trimmed;
17
+ }
18
+ function validateClaudePath(claudePath) {
19
+ try {
20
+ const claudeDir = path.dirname(claudePath);
21
+ const env = { ...process.env, PATH: `${claudeDir}:${process.env.PATH ?? ""}` };
22
+ const out = execSync(`"${claudePath}" --version`, { encoding: "utf8", stdio: "pipe", timeout: 1e4, env }).trim();
23
+ return { ok: true, version: out.split("\n")[0].slice(0, 30) };
24
+ } catch {
25
+ return { ok: false };
26
+ }
27
+ }
28
+ function cacheClaudePath(claudePath, cacheDir) {
29
+ const dir = cacheDir ?? path.join(process.cwd(), ".sna");
30
+ try {
31
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
32
+ fs.writeFileSync(path.join(dir, "claude-path"), claudePath);
33
+ } catch {
34
+ }
35
+ }
36
+ function resolveClaudeCli(opts) {
37
+ const cacheDir = opts?.cacheDir;
38
+ if (process.env.SNA_CLAUDE_COMMAND) {
39
+ const v = validateClaudePath(process.env.SNA_CLAUDE_COMMAND);
40
+ return { path: process.env.SNA_CLAUDE_COMMAND, version: v.version, source: "env" };
41
+ }
42
+ const cacheFile = cacheDir ? path.join(cacheDir, "claude-path") : path.join(process.cwd(), ".sna/claude-path");
43
+ try {
44
+ const cached = fs.readFileSync(cacheFile, "utf8").trim();
45
+ if (cached) {
46
+ const v = validateClaudePath(cached);
47
+ if (v.ok) return { path: cached, version: v.version, source: "cache" };
20
48
  }
49
+ } catch {
21
50
  }
22
- for (const p of [
51
+ const staticPaths = [
23
52
  "/opt/homebrew/bin/claude",
24
53
  "/usr/local/bin/claude",
25
54
  `${process.env.HOME}/.local/bin/claude`,
26
- `${process.env.HOME}/.claude/bin/claude`
27
- ]) {
28
- try {
29
- execSync(`test -x "${p}"`, { stdio: "pipe" });
30
- return p;
31
- } catch {
55
+ `${process.env.HOME}/.claude/bin/claude`,
56
+ `${process.env.HOME}/.volta/bin/claude`
57
+ ];
58
+ for (const p of staticPaths) {
59
+ const v = validateClaudePath(p);
60
+ if (v.ok) {
61
+ cacheClaudePath(p, cacheDir);
62
+ return { path: p, version: v.version, source: "static" };
32
63
  }
33
64
  }
34
65
  try {
35
66
  const raw = execSync(`${SHELL} -i -l -c "command -v claude" 2>/dev/null`, { encoding: "utf8", timeout: 5e3 }).trim();
36
- const match = raw.match(/=(.+)/) ?? raw.match(/^(\/\S+)/m);
37
- return match ? match[1] : raw;
67
+ const resolved = parseCommandVOutput(raw);
68
+ if (resolved && resolved !== "claude") {
69
+ const v = validateClaudePath(resolved);
70
+ if (v.ok) {
71
+ cacheClaudePath(resolved, cacheDir);
72
+ return { path: resolved, version: v.version, source: "shell" };
73
+ }
74
+ }
38
75
  } catch {
39
- return "claude";
40
76
  }
77
+ return { path: "claude", source: "fallback" };
78
+ }
79
+ function resolveClaudePath(cwd) {
80
+ const result = resolveClaudeCli({ cacheDir: path.join(cwd, ".sna") });
81
+ logger.log("agent", `claude path: ${result.source}=${result.path}${result.version ? ` (${result.version})` : ""}`);
82
+ return result.path;
41
83
  }
42
84
  const _ClaudeCodeProcess = class _ClaudeCodeProcess {
43
85
  constructor(proc, options) {
@@ -473,6 +515,10 @@ class ClaudeCodeProvider {
473
515
  delete cleanEnv.CLAUDE_CODE_ENTRYPOINT;
474
516
  delete cleanEnv.CLAUDE_CODE_SESSION_ACCESS_TOKEN;
475
517
  delete cleanEnv.CLAUDE_CODE_OAUTH_TOKEN;
518
+ const claudeDir = path.dirname(claudePath);
519
+ if (claudeDir && claudeDir !== ".") {
520
+ cleanEnv.PATH = `${claudeDir}:${cleanEnv.PATH ?? ""}`;
521
+ }
476
522
  const proc = spawn(claudePath, [...claudePrefix, ...args], {
477
523
  cwd: options.cwd,
478
524
  env: cleanEnv,
@@ -483,5 +529,9 @@ class ClaudeCodeProvider {
483
529
  }
484
530
  }
485
531
  export {
486
- ClaudeCodeProvider
532
+ ClaudeCodeProvider,
533
+ cacheClaudePath,
534
+ parseCommandVOutput,
535
+ resolveClaudeCli,
536
+ validateClaudePath
487
537
  };