@sayknow-cli/utils 0.2.2

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 (65) hide show
  1. package/dist/types/abortable.d.ts +27 -0
  2. package/dist/types/async.d.ts +6 -0
  3. package/dist/types/cli.d.ts +118 -0
  4. package/dist/types/color.d.ts +82 -0
  5. package/dist/types/dirs.d.ts +163 -0
  6. package/dist/types/env.d.ts +68 -0
  7. package/dist/types/fetch-retry.d.ts +80 -0
  8. package/dist/types/format.d.ts +37 -0
  9. package/dist/types/frontmatter.d.ts +25 -0
  10. package/dist/types/fs-error.d.ts +31 -0
  11. package/dist/types/glob.d.ts +28 -0
  12. package/dist/types/hook-fetch.d.ts +16 -0
  13. package/dist/types/index.d.ts +30 -0
  14. package/dist/types/json.d.ts +4 -0
  15. package/dist/types/logger.d.ts +66 -0
  16. package/dist/types/mermaid-ascii.d.ts +11 -0
  17. package/dist/types/mime.d.ts +29 -0
  18. package/dist/types/peek-file.d.ts +9 -0
  19. package/dist/types/postmortem.d.ts +29 -0
  20. package/dist/types/procmgr.d.ts +35 -0
  21. package/dist/types/prompt.d.ts +18 -0
  22. package/dist/types/ptree.d.ts +108 -0
  23. package/dist/types/ring.d.ts +93 -0
  24. package/dist/types/safe-stderr.d.ts +1 -0
  25. package/dist/types/sanitize-text.d.ts +14 -0
  26. package/dist/types/snowflake.d.ts +25 -0
  27. package/dist/types/spawn-env.d.ts +4 -0
  28. package/dist/types/stream.d.ts +68 -0
  29. package/dist/types/tab-spacing.d.ts +9 -0
  30. package/dist/types/temp.d.ts +14 -0
  31. package/dist/types/type-guards.d.ts +3 -0
  32. package/dist/types/which.d.ts +37 -0
  33. package/package.json +61 -0
  34. package/src/abortable.ts +73 -0
  35. package/src/async.ts +50 -0
  36. package/src/cli.ts +439 -0
  37. package/src/color.ts +204 -0
  38. package/src/dirs.ts +539 -0
  39. package/src/env.ts +278 -0
  40. package/src/fetch-retry.ts +298 -0
  41. package/src/format.ts +112 -0
  42. package/src/frontmatter.ts +154 -0
  43. package/src/fs-error.ts +56 -0
  44. package/src/glob.ts +189 -0
  45. package/src/hook-fetch.ts +30 -0
  46. package/src/index.ts +50 -0
  47. package/src/json.ts +10 -0
  48. package/src/logger.ts +392 -0
  49. package/src/mermaid-ascii.ts +31 -0
  50. package/src/mime.ts +159 -0
  51. package/src/peek-file.ts +114 -0
  52. package/src/postmortem.ts +197 -0
  53. package/src/procmgr.ts +209 -0
  54. package/src/prompt.ts +471 -0
  55. package/src/ptree.ts +390 -0
  56. package/src/ring.ts +169 -0
  57. package/src/safe-stderr.ts +15 -0
  58. package/src/sanitize-text.ts +38 -0
  59. package/src/snowflake.ts +136 -0
  60. package/src/spawn-env.ts +23 -0
  61. package/src/stream.ts +403 -0
  62. package/src/tab-spacing.ts +312 -0
  63. package/src/temp.ts +77 -0
  64. package/src/type-guards.ts +11 -0
  65. package/src/which.ts +232 -0
package/src/dirs.ts ADDED
@@ -0,0 +1,539 @@
1
+ /**
2
+ * Centralized path helpers for sayknow-cli config directories.
3
+ *
4
+ * Uses PI_CONFIG_DIR (default ".skc") for the config root and
5
+ * PI_CODING_AGENT_DIR to override the agent directory.
6
+ *
7
+ * On Linux, if XDG_DATA_HOME / XDG_STATE_HOME / XDG_CACHE_HOME environment
8
+ * variables are set, paths are redirected to XDG-compliant locations under
9
+ * $XDG_*_HOME/skc/. This requires running `skc config migrate` first to
10
+ * move data to the new locations. No filesystem existence checks are performed
11
+ * — if the env var is set, skc trusts that the migration has been done.
12
+ */
13
+
14
+ import * as fs from "node:fs";
15
+ import * as os from "node:os";
16
+ import * as path from "node:path";
17
+ import { engines, version } from "../package.json" with { type: "json" };
18
+
19
+ /** App name (e.g. "skc") */
20
+ export const APP_NAME: string = "skc";
21
+
22
+ /** Config directory name (e.g. ".skc") */
23
+ export const CONFIG_DIR_NAME: string = ".skc";
24
+
25
+ /** Version (e.g. "1.0.0") */
26
+ export const VERSION: string = version;
27
+
28
+ /** Minimum Bun version */
29
+ export const MIN_BUN_VERSION: string = engines.bun.replace(/[^0-9.]/g, "");
30
+
31
+ /**
32
+ * Build the diagnostic shown when the Bun runtime executing `skc` is older
33
+ * than {@link MIN_BUN_VERSION}. This is the most common Windows native-install
34
+ * failure (issue #525): `bun install -g sayknow-cli` probes a recent Bun while
35
+ * the `skc` launcher resolves an older Bun still on PATH. The message names the
36
+ * exact detected runtime path and gives a platform-specific upgrade + PATH fix
37
+ * instead of a bare `bun upgrade`.
38
+ *
39
+ * Pure and platform-parameterized so it can be unit-tested cross-platform.
40
+ */
41
+ export function formatBunRuntimeError(opts: {
42
+ currentVersion: string;
43
+ minVersion: string;
44
+ execPath?: string;
45
+ platform?: NodeJS.Platform;
46
+ }): string {
47
+ const platform = opts.platform ?? process.platform;
48
+ const lines = [
49
+ `error: ${APP_NAME} requires Bun >= ${opts.minVersion}, but the running Bun is v${opts.currentVersion}.`,
50
+ ];
51
+ if (opts.execPath) {
52
+ lines.push(` detected Bun runtime: ${opts.execPath}`);
53
+ }
54
+ if (platform === "win32") {
55
+ lines.push(
56
+ "",
57
+ "The 'skc' launcher is using an older Bun than the one used to install it.",
58
+ "Upgrade Bun, then restart your terminal so PATH and the runtime refresh:",
59
+ "",
60
+ ' powershell -c "irm bun.sh/install.ps1|iex"',
61
+ "",
62
+ "After restarting the terminal, verify both versions match:",
63
+ " bun --version",
64
+ " skc --version",
65
+ "",
66
+ "If 'skc' still loads the old runtime, make sure %USERPROFILE%\\.bun\\bin is",
67
+ "first on PATH and remove any stale Bun installs shadowing it.",
68
+ );
69
+ } else {
70
+ lines.push(
71
+ "",
72
+ "Upgrade Bun, then restart your terminal:",
73
+ " bun upgrade",
74
+ "",
75
+ "Then verify:",
76
+ " bun --version",
77
+ " skc --version",
78
+ );
79
+ }
80
+ return `${lines.join("\n")}\n`;
81
+ }
82
+
83
+ // =============================================================================
84
+ // Project directory
85
+ // =============================================================================
86
+
87
+ /**
88
+ * On macOS, strip /private prefix only when both paths resolve to the same location.
89
+ * This preserves aliases like /private/tmp -> /tmp without rewriting unrelated paths.
90
+ */
91
+ function standardizeMacOSPath(p: string): string {
92
+ if (process.platform !== "darwin" || !p.startsWith("/private/")) return p;
93
+ const stripped = p.slice("/private".length);
94
+ try {
95
+ if (fs.realpathSync(p) === fs.realpathSync(stripped)) {
96
+ return stripped;
97
+ }
98
+ } catch {}
99
+ return p;
100
+ }
101
+
102
+ export function resolveEquivalentPath(inputPath: string): string {
103
+ const resolvedPath = path.resolve(inputPath);
104
+ try {
105
+ return fs.realpathSync(resolvedPath);
106
+ } catch {
107
+ return resolvedPath;
108
+ }
109
+ }
110
+
111
+ export function normalizePathForComparison(inputPath: string): string {
112
+ const resolvedPath = resolveEquivalentPath(inputPath);
113
+ return process.platform === "win32" ? resolvedPath.toLowerCase() : resolvedPath;
114
+ }
115
+
116
+ export function pathIsWithin(root: string, candidate: string): boolean {
117
+ const normalizedRoot = normalizePathForComparison(root);
118
+ const normalizedCandidate = normalizePathForComparison(candidate);
119
+ const relative = path.relative(normalizedRoot, normalizedCandidate);
120
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
121
+ }
122
+
123
+ export function relativePathWithinRoot(root: string, candidate: string): string | null {
124
+ if (!pathIsWithin(root, candidate)) return null;
125
+ const normalizedRoot = normalizePathForComparison(root);
126
+ const normalizedCandidate = normalizePathForComparison(candidate);
127
+ const relative = path.relative(normalizedRoot, normalizedCandidate);
128
+ return relative || null;
129
+ }
130
+
131
+ let projectDir = standardizeMacOSPath(process.cwd());
132
+
133
+ /** Get the project directory. */
134
+ export function getProjectDir(): string {
135
+ return projectDir;
136
+ }
137
+
138
+ /** Set the project directory. */
139
+ export function setProjectDir(dir: string): void {
140
+ projectDir = standardizeMacOSPath(path.resolve(dir));
141
+ process.chdir(projectDir);
142
+ }
143
+
144
+ /** Get the config directory name relative to home (e.g. ".skc" or PI_CONFIG_DIR override). */
145
+ export function getConfigDirName(): string {
146
+ return process.env.SKC_CONFIG_DIR ?? process.env.PI_CONFIG_DIR ?? CONFIG_DIR_NAME;
147
+ }
148
+
149
+ /** Get the config agent directory name relative to home (e.g. ".skc/agent" or PI_CONFIG_DIR + "/agent"). */
150
+ export function getConfigAgentDirName(): string {
151
+ return `${getConfigDirName()}/agent`;
152
+ }
153
+
154
+ // =============================================================================
155
+ // DirResolver — cached, XDG-aware path resolution
156
+ // =============================================================================
157
+
158
+ type XdgCategory = "data" | "state" | "cache";
159
+
160
+ /**
161
+ * Resolves and caches all sayknow-cli directory paths. On Linux, when XDG environment
162
+ * variables are set, paths are redirected under $XDG_*_HOME/skc/. A new
163
+ * instance is created whenever the agent directory changes, which naturally
164
+ * invalidates all cached paths.
165
+ */
166
+ class DirResolver {
167
+ readonly configRoot: string;
168
+ readonly agentDir: string;
169
+
170
+ // Per-category base dirs. Without XDG, all three equal configRoot / agentDir.
171
+ // With XDG on Linux, they point to $XDG_*_HOME/skc/.
172
+ readonly #rootDirs: Record<XdgCategory, string>;
173
+ readonly #agentDirs: Record<XdgCategory, string>;
174
+
175
+ readonly #rootCache = new Map<string, string>();
176
+ readonly #agentCache = new Map<string, string>();
177
+
178
+ constructor(agentDirOverride?: string) {
179
+ this.configRoot = path.join(os.homedir(), getConfigDirName());
180
+
181
+ const defaultAgent = path.join(this.configRoot, "agent");
182
+ this.agentDir = agentDirOverride ? path.resolve(agentDirOverride) : defaultAgent;
183
+ const isDefault = this.agentDir === defaultAgent;
184
+
185
+ // XDG is a Linux convention. On other platforms, or for non-default
186
+ // profiles, all categories resolve to the legacy paths.
187
+ let xdgData: string | undefined;
188
+ let xdgState: string | undefined;
189
+ let xdgCache: string | undefined;
190
+ if ((process.platform === "linux" || process.platform === "darwin") && isDefault) {
191
+ const resolveIf = (envVar: string) => {
192
+ const value = process.env[envVar];
193
+ if (value) {
194
+ try {
195
+ const joined = path.join(value, APP_NAME);
196
+ if (fs.existsSync(joined)) {
197
+ return joined;
198
+ }
199
+ } catch {}
200
+ }
201
+ return undefined;
202
+ };
203
+ xdgData = resolveIf("XDG_DATA_HOME");
204
+ xdgState = resolveIf("XDG_STATE_HOME");
205
+ xdgCache = resolveIf("XDG_CACHE_HOME");
206
+ }
207
+
208
+ this.#rootDirs = {
209
+ data: xdgData ?? this.configRoot,
210
+ state: xdgState ?? this.configRoot,
211
+ cache: xdgCache ?? this.configRoot,
212
+ };
213
+ // XDG flattens the agent/ prefix: ~/.skc/agent/sessions → $XDG_DATA_HOME/skc/sessions
214
+ this.#agentDirs = {
215
+ data: xdgData ?? this.agentDir,
216
+ state: xdgState ?? this.agentDir,
217
+ cache: xdgCache ?? this.agentDir,
218
+ };
219
+ }
220
+
221
+ /** Config-root subdirectory, with optional XDG override. */
222
+ rootSubdir(subdir: string, xdg?: XdgCategory): string {
223
+ const cached = this.#rootCache.get(subdir);
224
+ if (cached) return cached;
225
+ const base = xdg ? this.#rootDirs[xdg] : this.configRoot;
226
+ const result = path.join(base, subdir);
227
+ this.#rootCache.set(subdir, result);
228
+ return result;
229
+ }
230
+
231
+ /** Agent subdirectory, with optional XDG override. */
232
+ agentSubdir(userAgentDir: string | undefined, subdir: string, xdg?: XdgCategory): string {
233
+ if (!userAgentDir || userAgentDir === this.agentDir) {
234
+ const cached = this.#agentCache.get(subdir);
235
+ if (cached) return cached;
236
+ const base = xdg ? this.#agentDirs[xdg] : this.agentDir;
237
+ const result = path.join(base, subdir);
238
+ this.#agentCache.set(subdir, result);
239
+ return result;
240
+ }
241
+ return path.join(userAgentDir, subdir);
242
+ }
243
+ }
244
+
245
+ let dirs = new DirResolver(process.env.SKC_CODING_AGENT_DIR);
246
+
247
+ // Anchor home for the resolver. Captured at module load to stay stable across
248
+ // test mocks of `os.homedir()`. `getPluginsDir(home)` compares against this so
249
+ // production callers (`home === RESOLVER_HOME`) hit the XDG-aware resolver while
250
+ // tests passing a temp HOME short-circuit to a deterministic path.
251
+ const RESOLVER_HOME = os.homedir();
252
+
253
+ // =============================================================================
254
+ // Root directories
255
+ // =============================================================================
256
+
257
+ /** Get the config root directory (~/.skc). */
258
+ export function getConfigRootDir(): string {
259
+ return dirs.configRoot;
260
+ }
261
+
262
+ /** Set the coding agent directory. Creates a fresh resolver, invalidating all cached paths. */
263
+ export function setAgentDir(dir: string): void {
264
+ dirs = new DirResolver(dir);
265
+ process.env.SKC_CODING_AGENT_DIR = dir;
266
+ }
267
+
268
+ /** Get the agent config directory (~/.skc/agent). */
269
+ export function getAgentDir(): string {
270
+ return dirs.agentDir;
271
+ }
272
+
273
+ /** Get the project-local config directory (.skc). */
274
+ export function getProjectAgentDir(cwd: string = getProjectDir()): string {
275
+ return path.join(cwd, CONFIG_DIR_NAME);
276
+ }
277
+
278
+ // =============================================================================
279
+ // Config-root subdirectories (~/.skc/*)
280
+ // =============================================================================
281
+
282
+ /** Get the reports directory (~/.skc/reports). */
283
+ export function getReportsDir(): string {
284
+ return dirs.rootSubdir("reports", "state");
285
+ }
286
+
287
+ /** Get the logs directory (~/.skc/logs). */
288
+ export function getLogsDir(): string {
289
+ return dirs.rootSubdir("logs", "state");
290
+ }
291
+
292
+ /** Get the path to a dated log file (~/.skc/logs/skc.YYYY-MM-DD.log). */
293
+ export function getLogPath(date = new Date()): string {
294
+ return path.join(getLogsDir(), `${APP_NAME}.${date.toISOString().slice(0, 10)}.log`);
295
+ }
296
+
297
+ /**
298
+ * Get the plugins directory (~/.skc/plugins or its XDG equivalent).
299
+ *
300
+ * No-arg form (production callers) goes through the XDG-aware DirResolver so
301
+ * reads and writes always agree. The optional `home` parameter is for test
302
+ * isolation: when it differs from `os.homedir()` it short-circuits the resolver
303
+ * and returns `<home>/<configDir>/plugins` so tests with a temp HOME get a
304
+ * deterministic path. Passing `os.homedir()` explicitly is identical to the
305
+ * no-arg form — XDG semantics are preserved.
306
+ */
307
+ export function getPluginsDir(home?: string): string {
308
+ if (home !== undefined && home !== RESOLVER_HOME) {
309
+ return path.join(home, getConfigDirName(), "plugins");
310
+ }
311
+ return dirs.rootSubdir("plugins", "data");
312
+ }
313
+
314
+ /** Where npm installs packages (~/.skc/plugins/node_modules). */
315
+ export function getPluginsNodeModules(): string {
316
+ return path.join(getPluginsDir(), "node_modules");
317
+ }
318
+
319
+ /** Plugin manifest (~/.skc/plugins/package.json). */
320
+ export function getPluginsPackageJson(): string {
321
+ return path.join(getPluginsDir(), "package.json");
322
+ }
323
+
324
+ /** Plugin lock file (~/.skc/plugins/skc-plugins.lock.json). */
325
+ export function getPluginsLockfile(): string {
326
+ return path.join(getPluginsDir(), "skc-plugins.lock.json");
327
+ }
328
+
329
+ /** Get the remote mount directory (~/.skc/remote). */
330
+ export function getRemoteDir(): string {
331
+ return dirs.rootSubdir("remote", "data");
332
+ }
333
+
334
+ /** Get the agent-managed worktrees directory (~/.skc/wt). */
335
+ export function getWorktreesDir(): string {
336
+ return dirs.rootSubdir("wt", "data");
337
+ }
338
+
339
+ /** Get the SSH control socket directory (~/.skc/ssh-control). */
340
+ export function getSshControlDir(): string {
341
+ return dirs.rootSubdir("ssh-control", "state");
342
+ }
343
+
344
+ /** Get the remote host info directory (~/.skc/remote-host). */
345
+ export function getRemoteHostDir(): string {
346
+ return dirs.rootSubdir("remote-host", "data");
347
+ }
348
+
349
+ /** Get the managed Python venv directory (~/.skc/python-env). */
350
+ export function getPythonEnvDir(): string {
351
+ return dirs.rootSubdir("python-env", "data");
352
+ }
353
+
354
+ /** Get the shared Python gateway state directory (~/.skc/agent/python-gateway; XDG default: $XDG_STATE_HOME/skc/python-gateway). */
355
+ export function getPythonGatewayDir(): string {
356
+ return dirs.agentSubdir(undefined, "python-gateway", "state");
357
+ }
358
+
359
+ /** Get the puppeteer sandbox directory (~/.skc/puppeteer). */
360
+ export function getPuppeteerDir(): string {
361
+ return dirs.rootSubdir("puppeteer", "cache");
362
+ }
363
+
364
+ /**
365
+ * Stable 7-character hex digest of an absolute filesystem path.
366
+ *
367
+ * Used to pack the project identity into a single short fs-safe segment
368
+ * (e.g. PR-checkout and task-isolation worktree dirs under `~/.skc/wt/`).
369
+ * Bun.hash is non-cryptographic — collision space is ~2^28, which is fine
370
+ * for naming a handful of repos on a single machine. Same input on the
371
+ * same Bun runtime yields the same output.
372
+ */
373
+ export function hashPath(absPath: string): string {
374
+ return Bun.hash(path.resolve(absPath)).toString(16).padStart(16, "0").slice(-7);
375
+ }
376
+
377
+ /** Get the path to a single worktree directory (~/.skc/wt/<segment>). */
378
+ export function getWorktreeDir(segment: string): string {
379
+ return path.join(getWorktreesDir(), segment);
380
+ }
381
+
382
+ /** Get the GPU cache path (~/.skc/gpu_cache.json). */
383
+ export function getGpuCachePath(): string {
384
+ return dirs.rootSubdir("gpu_cache.json", "cache");
385
+ }
386
+
387
+ /**
388
+ * Get the GitHub view cache database path (~/.skc/cache/github-cache.db).
389
+ * Honors the `SKC_GITHUB_CACHE_DB` env var when set so tests can isolate the
390
+ * cache file without touching the rest of the config root.
391
+ */
392
+ export function getGithubCacheDbPath(): string {
393
+ const override = process.env.SKC_GITHUB_CACHE_DB;
394
+ if (override) return override;
395
+ return dirs.rootSubdir(path.join("cache", "github-cache.db"), "cache");
396
+ }
397
+
398
+ /** Get the natives directory (~/.skc/natives). */
399
+ export function getNativesDir(): string {
400
+ return dirs.rootSubdir("natives", "cache");
401
+ }
402
+
403
+ /** Get the stats database path (~/.skc/stats.db). */
404
+ export function getStatsDbPath(): string {
405
+ return dirs.rootSubdir("stats.db", "data");
406
+ }
407
+
408
+ /** Get the autoresearch state directory (~/.skc/autoresearch). */
409
+ export function getAutoresearchDir(): string {
410
+ return dirs.rootSubdir("autoresearch", "state");
411
+ }
412
+
413
+ /** Get the per-project autoresearch state directory (~/.skc/autoresearch/<encoded-project>). */
414
+ export function getAutoresearchProjectDir(encodedProject: string): string {
415
+ return path.join(getAutoresearchDir(), encodedProject);
416
+ }
417
+
418
+ /** Get the per-project autoresearch SQLite database path (~/.skc/autoresearch/<encoded-project>.db). */
419
+ export function getAutoresearchDbPath(encodedProject: string): string {
420
+ return path.join(getAutoresearchDir(), `${encodedProject}.db`);
421
+ }
422
+
423
+ /** Get the per-run artifact directory (~/.skc/autoresearch/<encoded-project>/runs/<runId>). */
424
+ export function getAutoresearchRunDir(encodedProject: string, runId: number): string {
425
+ return path.join(getAutoresearchProjectDir(encodedProject), "runs", String(runId).padStart(4, "0"));
426
+ }
427
+
428
+ // =============================================================================
429
+ // Agent subdirectories (~/.skc/agent/*)
430
+ // =============================================================================
431
+
432
+ /** Get the path to agent.db (SQLite database for settings and auth storage). */
433
+ export function getAgentDbPath(agentDir?: string): string {
434
+ return dirs.agentSubdir(agentDir, "agent.db", "data");
435
+ }
436
+
437
+ /** Get the path to history.db (SQLite database for session history). */
438
+ export function getHistoryDbPath(agentDir?: string): string {
439
+ return dirs.agentSubdir(agentDir, "history.db", "data");
440
+ }
441
+
442
+ /** Get the path to models.db (model cache database). */
443
+ export function getModelDbPath(agentDir?: string): string {
444
+ return dirs.agentSubdir(agentDir, "models.db", "data");
445
+ }
446
+
447
+ /** Get the sessions directory (~/.skc/agent/sessions). */
448
+ export function getSessionsDir(agentDir?: string): string {
449
+ return dirs.agentSubdir(agentDir, "sessions", "data");
450
+ }
451
+
452
+ /** Get the content-addressed blob store directory (~/.skc/agent/blobs). */
453
+ export function getBlobsDir(agentDir?: string): string {
454
+ return dirs.agentSubdir(agentDir, "blobs", "data");
455
+ }
456
+
457
+ /** Get the custom themes directory (~/.skc/agent/themes). */
458
+ export function getCustomThemesDir(agentDir?: string): string {
459
+ return dirs.agentSubdir(agentDir, "themes");
460
+ }
461
+
462
+ /** Get the tools directory (~/.skc/agent/tools). */
463
+ export function getToolsDir(agentDir?: string): string {
464
+ return dirs.agentSubdir(agentDir, "tools");
465
+ }
466
+
467
+ /** Get the slash commands directory (~/.skc/agent/commands). */
468
+ export function getCommandsDir(agentDir?: string): string {
469
+ return dirs.agentSubdir(agentDir, "commands");
470
+ }
471
+
472
+ /** Get the prompts directory (~/.skc/agent/prompts). */
473
+ export function getPromptsDir(agentDir?: string): string {
474
+ return dirs.agentSubdir(agentDir, "prompts");
475
+ }
476
+
477
+ /** Get the user-level Python modules directory (~/.skc/agent/modules). */
478
+ export function getAgentModulesDir(agentDir?: string): string {
479
+ return dirs.agentSubdir(agentDir, "modules");
480
+ }
481
+
482
+ /** Get the memories directory (~/.skc/agent/memories). */
483
+ export function getMemoriesDir(agentDir?: string): string {
484
+ return dirs.agentSubdir(agentDir, "memories", "state");
485
+ }
486
+
487
+ /** Get the terminal sessions directory (~/.skc/agent/terminal-sessions). */
488
+ export function getTerminalSessionsDir(agentDir?: string): string {
489
+ return dirs.agentSubdir(agentDir, "terminal-sessions", "state");
490
+ }
491
+
492
+ /** Get the crash log path (~/.skc/agent/skc-crash.log). */
493
+ export function getCrashLogPath(agentDir?: string): string {
494
+ return dirs.agentSubdir(agentDir, "skc-crash.log", "state");
495
+ }
496
+
497
+ /** Get the debug log path (~/.skc/agent/skc-debug.log). */
498
+ export function getDebugLogPath(agentDir?: string): string {
499
+ return dirs.agentSubdir(agentDir, `${APP_NAME}-debug.log`, "state");
500
+ }
501
+
502
+ // =============================================================================
503
+ // Project subdirectories (.skc/*)
504
+ // =============================================================================
505
+
506
+ /** Get the project-level Python modules directory (.skc/modules). */
507
+ export function getProjectModulesDir(cwd: string = getProjectDir()): string {
508
+ return path.join(getProjectAgentDir(cwd), "modules");
509
+ }
510
+
511
+ /** Get the project-level prompts directory (.skc/prompts). */
512
+ export function getProjectPromptsDir(cwd: string = getProjectDir()): string {
513
+ return path.join(getProjectAgentDir(cwd), "prompts");
514
+ }
515
+
516
+ /** Get the project-level plugin overrides path (.skc/plugin-overrides.json). */
517
+ export function getProjectPluginOverridesPath(cwd: string = getProjectDir()): string {
518
+ return path.join(getProjectAgentDir(cwd), "plugin-overrides.json");
519
+ }
520
+
521
+ // =============================================================================
522
+ // MCP config paths
523
+ // =============================================================================
524
+
525
+ /** Get the primary MCP config file path (first candidate). */
526
+ export function getMCPConfigPath(scope: "user" | "project", cwd: string = getProjectDir()): string {
527
+ if (scope === "user") {
528
+ return path.join(getAgentDir(), "mcp.json");
529
+ }
530
+ return path.join(getProjectAgentDir(cwd), "mcp.json");
531
+ }
532
+
533
+ /** Get the SSH config file path. */
534
+ export function getSSHConfigPath(scope: "user" | "project", cwd: string = getProjectDir()): string {
535
+ if (scope === "user") {
536
+ return path.join(getAgentDir(), "ssh.json");
537
+ }
538
+ return path.join(getProjectAgentDir(cwd), "ssh.json");
539
+ }