skimpyclaw 0.3.14 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/README.md +47 -37
  2. package/dist/__tests__/adapter-types.test.d.ts +4 -0
  3. package/dist/__tests__/adapter-types.test.js +63 -0
  4. package/dist/__tests__/anthropic-adapter.test.d.ts +4 -0
  5. package/dist/__tests__/anthropic-adapter.test.js +264 -0
  6. package/dist/__tests__/api.test.js +0 -1
  7. package/dist/__tests__/cli.integration.test.js +2 -4
  8. package/dist/__tests__/cli.test.js +0 -1
  9. package/dist/__tests__/code-agents-notifications.test.js +137 -0
  10. package/dist/__tests__/code-agents-parser.test.js +19 -1
  11. package/dist/__tests__/code-agents-preflight.test.js +3 -28
  12. package/dist/__tests__/code-agents-utils.test.js +34 -9
  13. package/dist/__tests__/code-agents-worktrees.test.js +116 -0
  14. package/dist/__tests__/codex-adapter.test.js +184 -0
  15. package/dist/__tests__/codex-auth.test.js +66 -0
  16. package/dist/__tests__/codex-provider-gating.test.js +35 -0
  17. package/dist/__tests__/codex-unified-loop.test.js +111 -0
  18. package/dist/__tests__/config-security.test.js +127 -0
  19. package/dist/__tests__/config.test.js +23 -0
  20. package/dist/__tests__/context-manager.test.js +243 -164
  21. package/dist/__tests__/cron-run.test.js +250 -0
  22. package/dist/__tests__/cron.test.js +12 -38
  23. package/dist/__tests__/digests.test.js +67 -0
  24. package/dist/__tests__/discord-attachments.test.js +211 -0
  25. package/dist/__tests__/discord-docs.test.d.ts +1 -0
  26. package/dist/__tests__/discord-docs.test.js +27 -0
  27. package/dist/__tests__/discord-thread-agents.test.d.ts +1 -0
  28. package/dist/__tests__/discord-thread-agents.test.js +115 -0
  29. package/dist/__tests__/discord-thread-context.test.d.ts +1 -0
  30. package/dist/__tests__/discord-thread-context.test.js +42 -0
  31. package/dist/__tests__/doctor.formatters.test.js +4 -4
  32. package/dist/__tests__/doctor.index.test.js +1 -1
  33. package/dist/__tests__/doctor.runner.test.js +3 -15
  34. package/dist/__tests__/env-sanitizer.test.d.ts +1 -0
  35. package/dist/__tests__/env-sanitizer.test.js +45 -0
  36. package/dist/__tests__/exec-approval.test.js +61 -0
  37. package/dist/__tests__/fetch-tool.test.d.ts +1 -0
  38. package/dist/__tests__/fetch-tool.test.js +85 -0
  39. package/dist/__tests__/gateway-status-auth.test.d.ts +1 -0
  40. package/dist/__tests__/gateway-status-auth.test.js +72 -0
  41. package/dist/__tests__/heartbeat.test.js +3 -3
  42. package/dist/__tests__/interactive-sessions.test.d.ts +1 -0
  43. package/dist/__tests__/interactive-sessions.test.js +96 -0
  44. package/dist/__tests__/langfuse.test.js +6 -18
  45. package/dist/__tests__/model-selection.test.js +3 -4
  46. package/dist/__tests__/providers-init.test.js +2 -8
  47. package/dist/__tests__/providers-routing.test.js +1 -1
  48. package/dist/__tests__/providers-utils.test.js +13 -3
  49. package/dist/__tests__/sessions.test.js +14 -10
  50. package/dist/__tests__/setup.test.js +12 -29
  51. package/dist/__tests__/skills.test.js +10 -7
  52. package/dist/__tests__/stream-formatter.test.d.ts +1 -0
  53. package/dist/__tests__/stream-formatter.test.js +114 -0
  54. package/dist/__tests__/token-efficiency.test.js +131 -15
  55. package/dist/__tests__/tool-loop.test.d.ts +4 -0
  56. package/dist/__tests__/tool-loop.test.js +505 -0
  57. package/dist/__tests__/tools.test.js +101 -276
  58. package/dist/__tests__/utils.test.d.ts +1 -0
  59. package/dist/__tests__/utils.test.js +14 -0
  60. package/dist/__tests__/voice.test.js +21 -0
  61. package/dist/agent.js +35 -4
  62. package/dist/api.js +113 -37
  63. package/dist/channels/discord/attachments.d.ts +50 -0
  64. package/dist/channels/discord/attachments.js +137 -0
  65. package/dist/channels/discord/delegation.d.ts +5 -0
  66. package/dist/channels/discord/delegation.js +136 -0
  67. package/dist/channels/discord/handlers.js +694 -7
  68. package/dist/channels/discord/index.d.ts +16 -1
  69. package/dist/channels/discord/index.js +64 -1
  70. package/dist/channels/discord/thread-agents.d.ts +54 -0
  71. package/dist/channels/discord/thread-agents.js +323 -0
  72. package/dist/channels/discord/threads.d.ts +58 -0
  73. package/dist/channels/discord/threads.js +192 -0
  74. package/dist/channels/discord/types.js +4 -2
  75. package/dist/channels/discord/utils.d.ts +16 -0
  76. package/dist/channels/discord/utils.js +86 -6
  77. package/dist/channels/telegram/index.d.ts +1 -1
  78. package/dist/channels/telegram/types.js +1 -1
  79. package/dist/channels/telegram/utils.js +9 -3
  80. package/dist/channels.d.ts +1 -1
  81. package/dist/cli.js +20 -400
  82. package/dist/code-agents/executor.d.ts +1 -1
  83. package/dist/code-agents/executor.js +101 -45
  84. package/dist/code-agents/index.d.ts +2 -7
  85. package/dist/code-agents/index.js +111 -80
  86. package/dist/code-agents/interactive-resume.d.ts +6 -0
  87. package/dist/code-agents/interactive-resume.js +98 -0
  88. package/dist/code-agents/interactive-sessions.d.ts +20 -0
  89. package/dist/code-agents/interactive-sessions.js +132 -0
  90. package/dist/code-agents/parser.js +5 -1
  91. package/dist/code-agents/registry.d.ts +7 -1
  92. package/dist/code-agents/registry.js +11 -23
  93. package/dist/code-agents/stream-formatter.d.ts +8 -0
  94. package/dist/code-agents/stream-formatter.js +92 -0
  95. package/dist/code-agents/types.d.ts +16 -24
  96. package/dist/code-agents/utils.d.ts +35 -11
  97. package/dist/code-agents/utils.js +349 -95
  98. package/dist/code-agents/worktrees.d.ts +37 -0
  99. package/dist/code-agents/worktrees.js +116 -0
  100. package/dist/config.d.ts +2 -4
  101. package/dist/config.js +123 -23
  102. package/dist/cron.d.ts +1 -6
  103. package/dist/cron.js +175 -82
  104. package/dist/dashboard/assets/index-B345aOO-.js +65 -0
  105. package/dist/dashboard/assets/index-ZWK4dalJ.css +1 -0
  106. package/dist/dashboard/index.html +2 -2
  107. package/dist/digests.d.ts +1 -0
  108. package/dist/digests.js +132 -42
  109. package/dist/doctor/checks.d.ts +0 -3
  110. package/dist/doctor/checks.js +1 -108
  111. package/dist/doctor/runner.js +1 -4
  112. package/dist/env-sanitizer.d.ts +2 -0
  113. package/dist/env-sanitizer.js +61 -0
  114. package/dist/exec-approval.d.ts +11 -1
  115. package/dist/exec-approval.js +17 -4
  116. package/dist/gateway.d.ts +3 -1
  117. package/dist/gateway.js +17 -7
  118. package/dist/heartbeat.js +1 -6
  119. package/dist/langfuse.js +3 -29
  120. package/dist/model-selection.js +3 -1
  121. package/dist/providers/adapter.d.ts +118 -0
  122. package/dist/providers/adapter.js +6 -0
  123. package/dist/providers/adapters/anthropic-adapter.d.ts +22 -0
  124. package/dist/providers/adapters/anthropic-adapter.js +204 -0
  125. package/dist/providers/adapters/codex-adapter.d.ts +26 -0
  126. package/dist/providers/adapters/codex-adapter.js +203 -0
  127. package/dist/providers/anthropic.d.ts +1 -0
  128. package/dist/providers/anthropic.js +10 -272
  129. package/dist/providers/codex.d.ts +21 -0
  130. package/dist/providers/codex.js +149 -330
  131. package/dist/providers/content.d.ts +1 -1
  132. package/dist/providers/content.js +2 -2
  133. package/dist/providers/context-manager.d.ts +18 -6
  134. package/dist/providers/context-manager.js +199 -223
  135. package/dist/providers/index.d.ts +9 -1
  136. package/dist/providers/index.js +73 -64
  137. package/dist/providers/loop-utils.d.ts +20 -0
  138. package/dist/providers/loop-utils.js +30 -0
  139. package/dist/providers/tool-loop.d.ts +12 -0
  140. package/dist/providers/tool-loop.js +251 -0
  141. package/dist/providers/utils.d.ts +19 -3
  142. package/dist/providers/utils.js +100 -29
  143. package/dist/secure-store.d.ts +8 -0
  144. package/dist/secure-store.js +80 -0
  145. package/dist/service.js +3 -28
  146. package/dist/sessions.d.ts +3 -0
  147. package/dist/sessions.js +147 -18
  148. package/dist/setup-templates.js +13 -25
  149. package/dist/setup.d.ts +10 -6
  150. package/dist/setup.js +84 -292
  151. package/dist/skills.js +3 -11
  152. package/dist/tools/agent-delegation.d.ts +19 -0
  153. package/dist/tools/agent-delegation.js +49 -0
  154. package/dist/tools/bash-tool.js +89 -34
  155. package/dist/tools/definitions.d.ts +199 -302
  156. package/dist/tools/definitions.js +70 -123
  157. package/dist/tools/execute-context.d.ts +13 -4
  158. package/dist/tools/fetch-tool.js +109 -13
  159. package/dist/tools/file-tools.js +7 -1
  160. package/dist/tools.d.ts +7 -7
  161. package/dist/tools.js +133 -151
  162. package/dist/types.d.ts +37 -30
  163. package/dist/utils.js +4 -6
  164. package/dist/voice.d.ts +1 -1
  165. package/dist/voice.js +17 -4
  166. package/package.json +33 -23
  167. package/templates/TOOLS.md +0 -27
  168. package/dist/__tests__/audit.test.js +0 -122
  169. package/dist/__tests__/code-agents-orchestrator.test.js +0 -216
  170. package/dist/__tests__/code-agents-sandbox.test.js +0 -163
  171. package/dist/__tests__/orchestrator.test.js +0 -425
  172. package/dist/__tests__/sandbox-bridge.test.js +0 -116
  173. package/dist/__tests__/sandbox-manager.test.js +0 -144
  174. package/dist/__tests__/sandbox-mount-security.test.js +0 -139
  175. package/dist/__tests__/sandbox-runtime.test.js +0 -176
  176. package/dist/__tests__/subagent.test.js +0 -240
  177. package/dist/__tests__/telegram.test.js +0 -42
  178. package/dist/code-agents/orchestrator.d.ts +0 -29
  179. package/dist/code-agents/orchestrator.js +0 -694
  180. package/dist/code-agents/worktree.d.ts +0 -40
  181. package/dist/code-agents/worktree.js +0 -215
  182. package/dist/dashboard/assets/index-BoTHPby4.js +0 -65
  183. package/dist/dashboard/assets/index-D4mufvBg.css +0 -1
  184. package/dist/dashboard.d.ts +0 -8
  185. package/dist/dashboard.js +0 -4071
  186. package/dist/discord.d.ts +0 -8
  187. package/dist/discord.js +0 -792
  188. package/dist/mcp-context-a8c.d.ts +0 -13
  189. package/dist/mcp-context-a8c.js +0 -34
  190. package/dist/orchestrator.d.ts +0 -15
  191. package/dist/orchestrator.js +0 -676
  192. package/dist/providers/openai.d.ts +0 -10
  193. package/dist/providers/openai.js +0 -355
  194. package/dist/sandbox/bridge.d.ts +0 -5
  195. package/dist/sandbox/bridge.js +0 -63
  196. package/dist/sandbox/index.d.ts +0 -5
  197. package/dist/sandbox/index.js +0 -4
  198. package/dist/sandbox/manager.d.ts +0 -7
  199. package/dist/sandbox/manager.js +0 -100
  200. package/dist/sandbox/mount-security.d.ts +0 -12
  201. package/dist/sandbox/mount-security.js +0 -122
  202. package/dist/sandbox/runtime.d.ts +0 -39
  203. package/dist/sandbox/runtime.js +0 -192
  204. package/dist/sandbox-utils.d.ts +0 -6
  205. package/dist/sandbox-utils.js +0 -36
  206. package/dist/subagent.d.ts +0 -19
  207. package/dist/subagent.js +0 -407
  208. package/dist/telegram.d.ts +0 -2
  209. package/dist/telegram.js +0 -11
  210. package/dist/tools/browser-tool.d.ts +0 -3
  211. package/dist/tools/browser-tool.js +0 -266
  212. package/sandbox/Dockerfile +0 -40
  213. /package/dist/__tests__/{audit.test.d.ts → code-agents-notifications.test.d.ts} +0 -0
  214. /package/dist/__tests__/{code-agents-orchestrator.test.d.ts → code-agents-worktrees.test.d.ts} +0 -0
  215. /package/dist/__tests__/{code-agents-sandbox.test.d.ts → codex-adapter.test.d.ts} +0 -0
  216. /package/dist/__tests__/{orchestrator.test.d.ts → codex-auth.test.d.ts} +0 -0
  217. /package/dist/__tests__/{sandbox-bridge.test.d.ts → codex-provider-gating.test.d.ts} +0 -0
  218. /package/dist/__tests__/{sandbox-manager.test.d.ts → codex-unified-loop.test.d.ts} +0 -0
  219. /package/dist/__tests__/{sandbox-mount-security.test.d.ts → config-security.test.d.ts} +0 -0
  220. /package/dist/__tests__/{sandbox-runtime.test.d.ts → cron-run.test.d.ts} +0 -0
  221. /package/dist/__tests__/{subagent.test.d.ts → digests.test.d.ts} +0 -0
  222. /package/dist/__tests__/{telegram.test.d.ts → discord-attachments.test.d.ts} +0 -0
@@ -0,0 +1,116 @@
1
+ import { execFileSync } from 'child_process';
2
+ import { existsSync, mkdirSync, realpathSync } from 'fs';
3
+ import { homedir } from 'os';
4
+ import { basename, join, relative, resolve } from 'path';
5
+ export function normalizeWorktreeRequest(value) {
6
+ if (value === true || value === false || value === 'auto')
7
+ return value;
8
+ if (typeof value !== 'string')
9
+ return undefined;
10
+ const normalized = value.trim().toLowerCase();
11
+ if (['true', 'yes', 'on', '1'].includes(normalized))
12
+ return true;
13
+ if (['false', 'no', 'off', '0'].includes(normalized))
14
+ return false;
15
+ if (normalized === 'auto')
16
+ return 'auto';
17
+ return undefined;
18
+ }
19
+ export function shouldAutoWorktreeTask(task) {
20
+ return /\b(rebase|review|re-review|pr review|pull request|compare|diff)\b/i.test(task)
21
+ || /https?:\/\/[^\s]+\/pull\/\d+/i.test(task);
22
+ }
23
+ export function shouldUseCodeAgentWorktree(task, request, config) {
24
+ if (request === false)
25
+ return false;
26
+ if (request === true)
27
+ return true;
28
+ if (config?.enabled === false || config?.mode === 'off')
29
+ return false;
30
+ if (config?.mode === 'always')
31
+ return true;
32
+ return shouldAutoWorktreeTask(task);
33
+ }
34
+ function expandWorktreeRoot(root) {
35
+ const raw = root?.trim() || '~/.skimpyclaw/worktrees';
36
+ if (raw === '~')
37
+ return homedir();
38
+ if (raw.startsWith('~/'))
39
+ return join(homedir(), raw.slice(2));
40
+ return raw.replace(/\$\{HOME\}/g, homedir());
41
+ }
42
+ function slug(value) {
43
+ return value.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'repo';
44
+ }
45
+ function git(args, cwd) {
46
+ return execFileSync('git', ['-C', cwd, ...args], {
47
+ encoding: 'utf-8',
48
+ stdio: ['ignore', 'pipe', 'pipe'],
49
+ }).trim();
50
+ }
51
+ export function findGitRoot(workdir) {
52
+ try {
53
+ return git(['rev-parse', '--show-toplevel'], workdir);
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ }
59
+ export function prepareCodeAgentWorktree(options) {
60
+ const sourceWorkdir = realpathSync(resolve(options.sourceWorkdir));
61
+ const rawGitRoot = findGitRoot(sourceWorkdir);
62
+ if (!rawGitRoot) {
63
+ if (options.required) {
64
+ throw new Error(`Cannot create worktree: ${sourceWorkdir} is not inside a git repository.`);
65
+ }
66
+ return undefined;
67
+ }
68
+ const gitRoot = realpathSync(rawGitRoot);
69
+ const worktreeRef = git(['rev-parse', '--verify', 'HEAD'], gitRoot);
70
+ const root = resolve(expandWorktreeRoot(options.config?.root));
71
+ const worktreePath = join(root, slug(basename(gitRoot)), options.id);
72
+ if (existsSync(worktreePath)) {
73
+ throw new Error(`Cannot create worktree: ${worktreePath} already exists.`);
74
+ }
75
+ mkdirSync(join(root, slug(basename(gitRoot))), { recursive: true });
76
+ git(['worktree', 'add', '--detach', worktreePath, worktreeRef], gitRoot);
77
+ const rel = relative(gitRoot, sourceWorkdir);
78
+ const runWorkdir = rel && !rel.startsWith('..') && rel !== '.'
79
+ ? join(worktreePath, rel)
80
+ : worktreePath;
81
+ return { sourceWorkdir, worktreePath, runWorkdir, worktreeRef, gitRoot };
82
+ }
83
+ export function cleanupCodeAgentWorktree(options) {
84
+ const at = new Date().toISOString();
85
+ const path = options.worktreePath;
86
+ if (!path)
87
+ return { status: 'skipped', reason: 'no worktree', at };
88
+ if (options.config?.cleanup === false) {
89
+ return { status: 'preserved', path, reason: 'cleanup disabled', at };
90
+ }
91
+ if (!existsSync(path))
92
+ return { status: 'skipped', path, reason: 'already removed', at };
93
+ try {
94
+ const worktreeRoot = realpathSync(path);
95
+ const status = git(['status', '--porcelain'], worktreeRoot);
96
+ if (status.trim()) {
97
+ return { status: 'preserved', path, reason: 'worktree has uncommitted changes', at };
98
+ }
99
+ const currentRef = git(['rev-parse', '--verify', 'HEAD'], worktreeRoot);
100
+ if (options.worktreeRef && currentRef !== options.worktreeRef) {
101
+ return { status: 'preserved', path, reason: 'worktree HEAD changed', at };
102
+ }
103
+ const sourceGitRoot = options.sourceWorkdir ? findGitRoot(options.sourceWorkdir) : null;
104
+ const controller = sourceGitRoot || worktreeRoot;
105
+ git(['worktree', 'remove', '--force', worktreeRoot], controller);
106
+ return { status: 'removed', path, at };
107
+ }
108
+ catch (err) {
109
+ return {
110
+ status: 'failed',
111
+ path,
112
+ reason: err instanceof Error ? err.message : String(err),
113
+ at,
114
+ };
115
+ }
116
+ }
package/dist/config.d.ts CHANGED
@@ -18,10 +18,8 @@ export declare function listMemoryFiles(agentId: string): {
18
18
  size: number;
19
19
  }[];
20
20
  /**
21
- * Resolve allowed paths for a given context. Priority:
22
- * 1. Explicit toolConfig.allowedPaths (if provided)
23
- * 2. Config top-level allowedPaths
24
- * 3. Fallback: ~/.skimpyclaw only
21
+ * Resolve allowed paths for a given context. Named project paths are always
22
+ * appended so channel-level tool overrides cannot accidentally hide them.
25
23
  */
26
24
  export declare function resolveAllowedPaths(config: Config, overridePaths?: string[]): string[];
27
25
  export declare function readMemoryFile(agentId: string, filename: string): string;
package/dist/config.js CHANGED
@@ -1,26 +1,126 @@
1
1
  // Config loader with environment variable expansion
2
- import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'fs';
2
+ import { readFileSync, writeFileSync, existsSync, readdirSync, statSync, chmodSync, mkdirSync } from 'fs';
3
3
  import { randomUUID } from 'crypto';
4
4
  import { homedir } from 'os';
5
5
  import { join, basename } from 'path';
6
6
  import dotenv from 'dotenv';
7
+ import { getSecureValue, setSecureValue, requireSecureStore } from './secure-store.js';
7
8
  const CONFIG_PATH = join(homedir(), '.skimpyclaw', 'config.json');
9
+ const CONFIG_DIR = join(homedir(), '.skimpyclaw');
8
10
  const ENV_PATH = join(homedir(), '.skimpyclaw', '.env');
11
+ const CONFIG_SECRET_SERVICE = 'skimpyclaw-config';
9
12
  let envLoaded = false;
13
+ const keychainCache = new Map();
14
+ const SECRET_KEY_PATTERN = /(api.?key|token|secret|password|auth.?token|private.?key)/i;
15
+ let warnedSecureStoreUnavailable = false;
10
16
  function ensureEnvLoaded() {
11
17
  if (envLoaded)
12
18
  return;
13
19
  dotenv.config({ path: ENV_PATH });
14
20
  envLoaded = true;
15
21
  }
22
+ function resolveKeychainReference(raw) {
23
+ const [service, ...accountParts] = raw.split('/');
24
+ const account = accountParts.join('/');
25
+ if (!service || !account) {
26
+ throw new Error(`[config] Invalid keychain reference: ${raw} (expected service/account)`);
27
+ }
28
+ const cacheKey = `${service}/${account}`;
29
+ const cached = keychainCache.get(cacheKey);
30
+ if (cached !== undefined) {
31
+ return cached;
32
+ }
33
+ const value = getSecureValue(service, account);
34
+ if (value === null) {
35
+ throw new Error(`[config] Keychain secret not found for ${cacheKey}. Re-run setup or add it with: security add-generic-password -U -s ${service} -a ${account} -w '<secret>'`);
36
+ }
37
+ keychainCache.set(cacheKey, value);
38
+ return value;
39
+ }
40
+ function expandStringReferences(value) {
41
+ return value.replace(/\$\{([^}]+)\}/g, (_, token) => {
42
+ if (token.startsWith('KEYCHAIN:')) {
43
+ return resolveKeychainReference(token.slice('KEYCHAIN:'.length));
44
+ }
45
+ if (process.env[token] === undefined) {
46
+ console.warn(`[config] env var \${${token}} is not set`);
47
+ }
48
+ return process.env[token] || '';
49
+ });
50
+ }
51
+ function isSecureReference(value) {
52
+ return value.startsWith('${') && value.endsWith('}');
53
+ }
54
+ function shouldSecureField(path) {
55
+ const key = path[path.length - 1] || '';
56
+ return SECRET_KEY_PATTERN.test(key);
57
+ }
58
+ function sanitizeKeychainAccount(path) {
59
+ return path.join('.').replace(/[^\w.-]/g, '_');
60
+ }
61
+ function migratePlaintextSecrets(obj, path = []) {
62
+ if (Array.isArray(obj)) {
63
+ let migrated = 0;
64
+ const mapped = obj.map((item, index) => {
65
+ const result = migratePlaintextSecrets(item, [...path, String(index)]);
66
+ migrated += result.migrated;
67
+ return result.value;
68
+ });
69
+ return { value: mapped, migrated };
70
+ }
71
+ if (obj && typeof obj === 'object') {
72
+ const result = {};
73
+ let migrated = 0;
74
+ for (const [key, value] of Object.entries(obj)) {
75
+ const migratedValue = migratePlaintextSecrets(value, [...path, key]);
76
+ result[key] = migratedValue.value;
77
+ migrated += migratedValue.migrated;
78
+ }
79
+ return { value: result, migrated };
80
+ }
81
+ if (typeof obj !== 'string') {
82
+ return { value: obj, migrated: 0 };
83
+ }
84
+ if (!shouldSecureField(path)) {
85
+ return { value: obj, migrated: 0 };
86
+ }
87
+ const trimmed = obj.trim();
88
+ if (!trimmed || trimmed === '[REDACTED]' || isSecureReference(trimmed)) {
89
+ return { value: obj, migrated: 0 };
90
+ }
91
+ if (process.platform !== 'darwin') {
92
+ if (!warnedSecureStoreUnavailable) {
93
+ warnedSecureStoreUnavailable = true;
94
+ console.warn('[config] Secure secret migration requires macOS Keychain. Plaintext values are preserved on this platform.');
95
+ }
96
+ return { value: obj, migrated: 0 };
97
+ }
98
+ requireSecureStore('Config secret migration');
99
+ const account = sanitizeKeychainAccount(path);
100
+ setSecureValue(CONFIG_SECRET_SERVICE, account, obj);
101
+ return {
102
+ value: `\${KEYCHAIN:${CONFIG_SECRET_SERVICE}/${account}}`,
103
+ migrated: 1,
104
+ };
105
+ }
106
+ function writeConfigFile(rawConfig) {
107
+ mkdirSync(CONFIG_DIR, { recursive: true });
108
+ writeFileSync(CONFIG_PATH, JSON.stringify(rawConfig, null, 2), { encoding: 'utf-8', mode: 0o600 });
109
+ chmodSync(CONFIG_PATH, 0o600);
110
+ }
111
+ function loadAndMaybeMigrateRawConfig() {
112
+ const raw = readFileSync(CONFIG_PATH, 'utf-8');
113
+ const parsed = JSON.parse(raw);
114
+ const migrated = migratePlaintextSecrets(parsed);
115
+ if (migrated.migrated > 0) {
116
+ writeConfigFile(migrated.value);
117
+ console.log(`[config] Migrated ${migrated.migrated} plaintext secret(s) to macOS Keychain`);
118
+ }
119
+ return migrated.value;
120
+ }
16
121
  function expandEnvVars(obj) {
17
122
  if (typeof obj === 'string') {
18
- return obj.replace(/\$\{(\w+)\}/g, (_, key) => {
19
- if (process.env[key] === undefined) {
20
- console.warn(`[config] env var \${${key}} is not set`);
21
- }
22
- return process.env[key] || '';
23
- });
123
+ return expandStringReferences(obj);
24
124
  }
25
125
  if (Array.isArray(obj)) {
26
126
  return obj.map(expandEnvVars);
@@ -39,8 +139,7 @@ export function loadConfig() {
39
139
  if (!existsSync(CONFIG_PATH)) {
40
140
  throw new Error(`Config not found: ${CONFIG_PATH}\nRun 'pnpm run setup' to create one.`);
41
141
  }
42
- const raw = readFileSync(CONFIG_PATH, 'utf-8');
43
- const parsed = JSON.parse(raw);
142
+ const parsed = loadAndMaybeMigrateRawConfig();
44
143
  return expandEnvVars(parsed);
45
144
  }
46
145
  export function loadRawConfig() {
@@ -48,8 +147,7 @@ export function loadRawConfig() {
48
147
  if (!existsSync(CONFIG_PATH)) {
49
148
  throw new Error(`Config not found: ${CONFIG_PATH}\nRun 'pnpm run setup' to create one.`);
50
149
  }
51
- const raw = readFileSync(CONFIG_PATH, 'utf-8');
52
- return JSON.parse(raw);
150
+ return loadAndMaybeMigrateRawConfig();
53
151
  }
54
152
  export function getConfigPath() {
55
153
  return CONFIG_PATH;
@@ -70,7 +168,8 @@ export function getSessionsDir() {
70
168
  return join(homedir(), '.skimpyclaw', 'sessions');
71
169
  }
72
170
  export function saveConfig(config) {
73
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
171
+ const migrated = migratePlaintextSecrets(config);
172
+ writeConfigFile(migrated.value);
74
173
  }
75
174
  /**
76
175
  * Ensures the config has a dashboard token. If not, generates one and saves it.
@@ -81,11 +180,12 @@ export function ensureDashboardToken(config) {
81
180
  return config.dashboard.token;
82
181
  }
83
182
  const token = randomUUID();
84
- // Read raw config to preserve env var references, then add the token
183
+ // Read raw config to preserve env var references, then add the token.
184
+ // loadRawConfig() already runs migratePlaintextSecrets(), so no need to run it again.
85
185
  const raw = loadRawConfig();
86
186
  raw.dashboard = raw.dashboard || {};
87
187
  raw.dashboard.token = token;
88
- writeFileSync(CONFIG_PATH, JSON.stringify(raw, null, 2), 'utf-8');
188
+ writeConfigFile(raw);
89
189
  // Update the in-memory config too
90
190
  if (!config.dashboard) {
91
191
  config.dashboard = {};
@@ -110,17 +210,17 @@ export function listMemoryFiles(agentId) {
110
210
  }).sort((a, b) => b.date.localeCompare(a.date));
111
211
  }
112
212
  /**
113
- * Resolve allowed paths for a given context. Priority:
114
- * 1. Explicit toolConfig.allowedPaths (if provided)
115
- * 2. Config top-level allowedPaths
116
- * 3. Fallback: ~/.skimpyclaw only
213
+ * Resolve allowed paths for a given context. Named project paths are always
214
+ * appended so channel-level tool overrides cannot accidentally hide them.
117
215
  */
118
216
  export function resolveAllowedPaths(config, overridePaths) {
119
- if (overridePaths?.length)
120
- return overridePaths;
121
- if (config.allowedPaths?.length)
122
- return config.allowedPaths;
123
- return [join(homedir(), '.skimpyclaw')];
217
+ const basePaths = overridePaths?.length
218
+ ? overridePaths
219
+ : config.allowedPaths?.length
220
+ ? config.allowedPaths
221
+ : [join(homedir(), '.skimpyclaw')];
222
+ const projectPaths = Object.values(config.projects || {});
223
+ return [...new Set([...basePaths, ...projectPaths])];
124
224
  }
125
225
  export function readMemoryFile(agentId, filename) {
126
226
  if (!isValidAgentId(agentId)) {
package/dist/cron.d.ts CHANGED
@@ -13,6 +13,7 @@ export declare function getCronRunStatus(): {
13
13
  running: string[];
14
14
  recent: CronLogEntry[];
15
15
  };
16
+ export declare function isRetryableCronAgentError(err: unknown): boolean;
16
17
  export declare function initCron(config: Config): void;
17
18
  /**
18
19
  * Parse dual-output format with ---VOICE--- and ---TEXT--- delimiters.
@@ -23,12 +24,6 @@ export declare function parseDualOutput(response: string): {
23
24
  voice: string | null;
24
25
  text: string;
25
26
  };
26
- /**
27
- * Post-run guard for the pr-review cron job.
28
- * Validates that the agent actually used code_with_agent when PRs were found.
29
- * Non-throwing — logs a warning and returns an alert message (or null if OK).
30
- */
31
- export declare function validatePrReviewOutput(output: string): string | null;
32
27
  export declare function getCronJobs(): {
33
28
  id: string;
34
29
  name: string;