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.
- package/README.md +47 -37
- package/dist/__tests__/adapter-types.test.d.ts +4 -0
- package/dist/__tests__/adapter-types.test.js +63 -0
- package/dist/__tests__/anthropic-adapter.test.d.ts +4 -0
- package/dist/__tests__/anthropic-adapter.test.js +264 -0
- package/dist/__tests__/api.test.js +0 -1
- package/dist/__tests__/cli.integration.test.js +2 -4
- package/dist/__tests__/cli.test.js +0 -1
- package/dist/__tests__/code-agents-notifications.test.js +137 -0
- package/dist/__tests__/code-agents-parser.test.js +19 -1
- package/dist/__tests__/code-agents-preflight.test.js +3 -28
- package/dist/__tests__/code-agents-utils.test.js +34 -9
- package/dist/__tests__/code-agents-worktrees.test.js +116 -0
- package/dist/__tests__/codex-adapter.test.js +184 -0
- package/dist/__tests__/codex-auth.test.js +66 -0
- package/dist/__tests__/codex-provider-gating.test.js +35 -0
- package/dist/__tests__/codex-unified-loop.test.js +111 -0
- package/dist/__tests__/config-security.test.js +127 -0
- package/dist/__tests__/config.test.js +23 -0
- package/dist/__tests__/context-manager.test.js +243 -164
- package/dist/__tests__/cron-run.test.js +250 -0
- package/dist/__tests__/cron.test.js +12 -38
- package/dist/__tests__/digests.test.js +67 -0
- package/dist/__tests__/discord-attachments.test.js +211 -0
- package/dist/__tests__/discord-docs.test.d.ts +1 -0
- package/dist/__tests__/discord-docs.test.js +27 -0
- package/dist/__tests__/discord-thread-agents.test.d.ts +1 -0
- package/dist/__tests__/discord-thread-agents.test.js +115 -0
- package/dist/__tests__/discord-thread-context.test.d.ts +1 -0
- package/dist/__tests__/discord-thread-context.test.js +42 -0
- package/dist/__tests__/doctor.formatters.test.js +4 -4
- package/dist/__tests__/doctor.index.test.js +1 -1
- package/dist/__tests__/doctor.runner.test.js +3 -15
- package/dist/__tests__/env-sanitizer.test.d.ts +1 -0
- package/dist/__tests__/env-sanitizer.test.js +45 -0
- package/dist/__tests__/exec-approval.test.js +61 -0
- package/dist/__tests__/fetch-tool.test.d.ts +1 -0
- package/dist/__tests__/fetch-tool.test.js +85 -0
- package/dist/__tests__/gateway-status-auth.test.d.ts +1 -0
- package/dist/__tests__/gateway-status-auth.test.js +72 -0
- package/dist/__tests__/heartbeat.test.js +3 -3
- package/dist/__tests__/interactive-sessions.test.d.ts +1 -0
- package/dist/__tests__/interactive-sessions.test.js +96 -0
- package/dist/__tests__/langfuse.test.js +6 -18
- package/dist/__tests__/model-selection.test.js +3 -4
- package/dist/__tests__/providers-init.test.js +2 -8
- package/dist/__tests__/providers-routing.test.js +1 -1
- package/dist/__tests__/providers-utils.test.js +13 -3
- package/dist/__tests__/sessions.test.js +14 -10
- package/dist/__tests__/setup.test.js +12 -29
- package/dist/__tests__/skills.test.js +10 -7
- package/dist/__tests__/stream-formatter.test.d.ts +1 -0
- package/dist/__tests__/stream-formatter.test.js +114 -0
- package/dist/__tests__/token-efficiency.test.js +131 -15
- package/dist/__tests__/tool-loop.test.d.ts +4 -0
- package/dist/__tests__/tool-loop.test.js +505 -0
- package/dist/__tests__/tools.test.js +101 -276
- package/dist/__tests__/utils.test.d.ts +1 -0
- package/dist/__tests__/utils.test.js +14 -0
- package/dist/__tests__/voice.test.js +21 -0
- package/dist/agent.js +35 -4
- package/dist/api.js +113 -37
- package/dist/channels/discord/attachments.d.ts +50 -0
- package/dist/channels/discord/attachments.js +137 -0
- package/dist/channels/discord/delegation.d.ts +5 -0
- package/dist/channels/discord/delegation.js +136 -0
- package/dist/channels/discord/handlers.js +694 -7
- package/dist/channels/discord/index.d.ts +16 -1
- package/dist/channels/discord/index.js +64 -1
- package/dist/channels/discord/thread-agents.d.ts +54 -0
- package/dist/channels/discord/thread-agents.js +323 -0
- package/dist/channels/discord/threads.d.ts +58 -0
- package/dist/channels/discord/threads.js +192 -0
- package/dist/channels/discord/types.js +4 -2
- package/dist/channels/discord/utils.d.ts +16 -0
- package/dist/channels/discord/utils.js +86 -6
- package/dist/channels/telegram/index.d.ts +1 -1
- package/dist/channels/telegram/types.js +1 -1
- package/dist/channels/telegram/utils.js +9 -3
- package/dist/channels.d.ts +1 -1
- package/dist/cli.js +20 -400
- package/dist/code-agents/executor.d.ts +1 -1
- package/dist/code-agents/executor.js +101 -45
- package/dist/code-agents/index.d.ts +2 -7
- package/dist/code-agents/index.js +111 -80
- package/dist/code-agents/interactive-resume.d.ts +6 -0
- package/dist/code-agents/interactive-resume.js +98 -0
- package/dist/code-agents/interactive-sessions.d.ts +20 -0
- package/dist/code-agents/interactive-sessions.js +132 -0
- package/dist/code-agents/parser.js +5 -1
- package/dist/code-agents/registry.d.ts +7 -1
- package/dist/code-agents/registry.js +11 -23
- package/dist/code-agents/stream-formatter.d.ts +8 -0
- package/dist/code-agents/stream-formatter.js +92 -0
- package/dist/code-agents/types.d.ts +16 -24
- package/dist/code-agents/utils.d.ts +35 -11
- package/dist/code-agents/utils.js +349 -95
- package/dist/code-agents/worktrees.d.ts +37 -0
- package/dist/code-agents/worktrees.js +116 -0
- package/dist/config.d.ts +2 -4
- package/dist/config.js +123 -23
- package/dist/cron.d.ts +1 -6
- package/dist/cron.js +175 -82
- package/dist/dashboard/assets/index-B345aOO-.js +65 -0
- package/dist/dashboard/assets/index-ZWK4dalJ.css +1 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/digests.d.ts +1 -0
- package/dist/digests.js +132 -42
- package/dist/doctor/checks.d.ts +0 -3
- package/dist/doctor/checks.js +1 -108
- package/dist/doctor/runner.js +1 -4
- package/dist/env-sanitizer.d.ts +2 -0
- package/dist/env-sanitizer.js +61 -0
- package/dist/exec-approval.d.ts +11 -1
- package/dist/exec-approval.js +17 -4
- package/dist/gateway.d.ts +3 -1
- package/dist/gateway.js +17 -7
- package/dist/heartbeat.js +1 -6
- package/dist/langfuse.js +3 -29
- package/dist/model-selection.js +3 -1
- package/dist/providers/adapter.d.ts +118 -0
- package/dist/providers/adapter.js +6 -0
- package/dist/providers/adapters/anthropic-adapter.d.ts +22 -0
- package/dist/providers/adapters/anthropic-adapter.js +204 -0
- package/dist/providers/adapters/codex-adapter.d.ts +26 -0
- package/dist/providers/adapters/codex-adapter.js +203 -0
- package/dist/providers/anthropic.d.ts +1 -0
- package/dist/providers/anthropic.js +10 -272
- package/dist/providers/codex.d.ts +21 -0
- package/dist/providers/codex.js +149 -330
- package/dist/providers/content.d.ts +1 -1
- package/dist/providers/content.js +2 -2
- package/dist/providers/context-manager.d.ts +18 -6
- package/dist/providers/context-manager.js +199 -223
- package/dist/providers/index.d.ts +9 -1
- package/dist/providers/index.js +73 -64
- package/dist/providers/loop-utils.d.ts +20 -0
- package/dist/providers/loop-utils.js +30 -0
- package/dist/providers/tool-loop.d.ts +12 -0
- package/dist/providers/tool-loop.js +251 -0
- package/dist/providers/utils.d.ts +19 -3
- package/dist/providers/utils.js +100 -29
- package/dist/secure-store.d.ts +8 -0
- package/dist/secure-store.js +80 -0
- package/dist/service.js +3 -28
- package/dist/sessions.d.ts +3 -0
- package/dist/sessions.js +147 -18
- package/dist/setup-templates.js +13 -25
- package/dist/setup.d.ts +10 -6
- package/dist/setup.js +84 -292
- package/dist/skills.js +3 -11
- package/dist/tools/agent-delegation.d.ts +19 -0
- package/dist/tools/agent-delegation.js +49 -0
- package/dist/tools/bash-tool.js +89 -34
- package/dist/tools/definitions.d.ts +199 -302
- package/dist/tools/definitions.js +70 -123
- package/dist/tools/execute-context.d.ts +13 -4
- package/dist/tools/fetch-tool.js +109 -13
- package/dist/tools/file-tools.js +7 -1
- package/dist/tools.d.ts +7 -7
- package/dist/tools.js +133 -151
- package/dist/types.d.ts +37 -30
- package/dist/utils.js +4 -6
- package/dist/voice.d.ts +1 -1
- package/dist/voice.js +17 -4
- package/package.json +33 -23
- package/templates/TOOLS.md +0 -27
- package/dist/__tests__/audit.test.js +0 -122
- package/dist/__tests__/code-agents-orchestrator.test.js +0 -216
- package/dist/__tests__/code-agents-sandbox.test.js +0 -163
- package/dist/__tests__/orchestrator.test.js +0 -425
- package/dist/__tests__/sandbox-bridge.test.js +0 -116
- package/dist/__tests__/sandbox-manager.test.js +0 -144
- package/dist/__tests__/sandbox-mount-security.test.js +0 -139
- package/dist/__tests__/sandbox-runtime.test.js +0 -176
- package/dist/__tests__/subagent.test.js +0 -240
- package/dist/__tests__/telegram.test.js +0 -42
- package/dist/code-agents/orchestrator.d.ts +0 -29
- package/dist/code-agents/orchestrator.js +0 -694
- package/dist/code-agents/worktree.d.ts +0 -40
- package/dist/code-agents/worktree.js +0 -215
- package/dist/dashboard/assets/index-BoTHPby4.js +0 -65
- package/dist/dashboard/assets/index-D4mufvBg.css +0 -1
- package/dist/dashboard.d.ts +0 -8
- package/dist/dashboard.js +0 -4071
- package/dist/discord.d.ts +0 -8
- package/dist/discord.js +0 -792
- package/dist/mcp-context-a8c.d.ts +0 -13
- package/dist/mcp-context-a8c.js +0 -34
- package/dist/orchestrator.d.ts +0 -15
- package/dist/orchestrator.js +0 -676
- package/dist/providers/openai.d.ts +0 -10
- package/dist/providers/openai.js +0 -355
- package/dist/sandbox/bridge.d.ts +0 -5
- package/dist/sandbox/bridge.js +0 -63
- package/dist/sandbox/index.d.ts +0 -5
- package/dist/sandbox/index.js +0 -4
- package/dist/sandbox/manager.d.ts +0 -7
- package/dist/sandbox/manager.js +0 -100
- package/dist/sandbox/mount-security.d.ts +0 -12
- package/dist/sandbox/mount-security.js +0 -122
- package/dist/sandbox/runtime.d.ts +0 -39
- package/dist/sandbox/runtime.js +0 -192
- package/dist/sandbox-utils.d.ts +0 -6
- package/dist/sandbox-utils.js +0 -36
- package/dist/subagent.d.ts +0 -19
- package/dist/subagent.js +0 -407
- package/dist/telegram.d.ts +0 -2
- package/dist/telegram.js +0 -11
- package/dist/tools/browser-tool.d.ts +0 -3
- package/dist/tools/browser-tool.js +0 -266
- package/sandbox/Dockerfile +0 -40
- /package/dist/__tests__/{audit.test.d.ts → code-agents-notifications.test.d.ts} +0 -0
- /package/dist/__tests__/{code-agents-orchestrator.test.d.ts → code-agents-worktrees.test.d.ts} +0 -0
- /package/dist/__tests__/{code-agents-sandbox.test.d.ts → codex-adapter.test.d.ts} +0 -0
- /package/dist/__tests__/{orchestrator.test.d.ts → codex-auth.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-bridge.test.d.ts → codex-provider-gating.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-manager.test.d.ts → codex-unified-loop.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-mount-security.test.d.ts → config-security.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-runtime.test.d.ts → cron-run.test.d.ts} +0 -0
- /package/dist/__tests__/{subagent.test.d.ts → digests.test.d.ts} +0 -0
- /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.
|
|
22
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
114
|
-
*
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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;
|