obol-ai 0.1.7 → 0.2.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/.claude/settings.local.json +2 -1
- package/bin/obol.js +8 -0
- package/package.json +1 -1
- package/src/background.js +8 -1
- package/src/bridge.js +13 -4
- package/src/claude.js +142 -21
- package/src/cli/delete.js +63 -0
- package/src/config.js +13 -2
- package/src/credentials.js +126 -0
- package/src/defaults/AGENTS.md +135 -0
- package/src/defaults/traits.json +8 -0
- package/src/evolve.js +90 -19
- package/src/index.js +9 -2
- package/src/memory.js +19 -11
- package/src/messages.js +17 -2
- package/src/personality.js +21 -1
- package/src/post-setup.js +8 -8
- package/src/sanitize.js +58 -0
- package/src/telegram.js +190 -102
- package/src/tenant.js +54 -18
- package/src/first-run.js +0 -117
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"Bash(/Users/jovinkenroye/Sites/obol/tests/mock-grammy.test.js:*)",
|
|
14
14
|
"Bash(git -C /Users/jovinkenroye/Sites/obol diff --stat)",
|
|
15
15
|
"Bash(git -C /Users/jovinkenroye/Sites/obol add:*)",
|
|
16
|
-
"Bash(git -C:*)"
|
|
16
|
+
"Bash(git -C:*)",
|
|
17
|
+
"Bash(pass ls:*)"
|
|
17
18
|
]
|
|
18
19
|
}
|
|
19
20
|
}
|
package/bin/obol.js
CHANGED
|
@@ -78,4 +78,12 @@ program
|
|
|
78
78
|
await upgrade();
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
+
program
|
|
82
|
+
.command('delete')
|
|
83
|
+
.description('Delete all OBOL data and start fresh')
|
|
84
|
+
.action(async () => {
|
|
85
|
+
const { delete: deleteAll } = require('../src/cli/delete');
|
|
86
|
+
await deleteAll();
|
|
87
|
+
});
|
|
88
|
+
|
|
81
89
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "obol-ai",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Self-evolving AI assistant that learns, remembers, and acts on its own. Persistent vector memory, self-rewriting personality, proactive heartbeats.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
package/src/background.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* Claude periodically reports progress back to the user.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
const CHECK_IN_INTERVAL = 30000;
|
|
8
|
+
const CHECK_IN_INTERVAL = 30000;
|
|
9
|
+
const MAX_CONCURRENT_TASKS = 3;
|
|
9
10
|
|
|
10
11
|
class BackgroundRunner {
|
|
11
12
|
constructor() {
|
|
@@ -21,6 +22,12 @@ class BackgroundRunner {
|
|
|
21
22
|
* @param {object} memory - Memory instance
|
|
22
23
|
*/
|
|
23
24
|
spawn(claude, task, ctx, memory) {
|
|
25
|
+
let running = 0;
|
|
26
|
+
for (const t of this.tasks.values()) {
|
|
27
|
+
if (t.status === 'running') running++;
|
|
28
|
+
}
|
|
29
|
+
if (running >= MAX_CONCURRENT_TASKS) return null;
|
|
30
|
+
|
|
24
31
|
const taskId = ++this.taskCounter;
|
|
25
32
|
const chatId = ctx.chat.id;
|
|
26
33
|
|
package/src/bridge.js
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
|
+
|
|
2
3
|
|
|
3
4
|
const BRIDGE_MAX_PER_HOUR = 20;
|
|
4
5
|
const bridgeUsage = new Map();
|
|
5
6
|
|
|
7
|
+
const _bridgeCleanup = setInterval(() => {
|
|
8
|
+
const hourAgo = Date.now() - 3600000;
|
|
9
|
+
for (const [key, timestamps] of bridgeUsage) {
|
|
10
|
+
const recent = timestamps.filter(ts => ts > hourAgo);
|
|
11
|
+
if (recent.length === 0) bridgeUsage.delete(key);
|
|
12
|
+
else bridgeUsage.set(key, recent);
|
|
13
|
+
}
|
|
14
|
+
}, 600000);
|
|
15
|
+
_bridgeCleanup.unref();
|
|
16
|
+
|
|
6
17
|
function checkBridgeRateLimit(userId) {
|
|
7
18
|
const now = Date.now();
|
|
8
19
|
const hourAgo = now - 3600000;
|
|
@@ -84,9 +95,7 @@ async function bridgeAsk(question, fromUserId, config, notifyFn, targetId) {
|
|
|
84
95
|
if (partner.personality?.user) systemParts.push(`\n## About Your Owner\n${partner.personality.user}`);
|
|
85
96
|
if (memoryContext) systemParts.push(memoryContext);
|
|
86
97
|
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
const response = await client.messages.create({
|
|
98
|
+
const response = await partner.claude.client.messages.create({
|
|
90
99
|
model: 'claude-sonnet-4-6',
|
|
91
100
|
max_tokens: 1024,
|
|
92
101
|
system: systemParts.join('\n'),
|
package/src/claude.js
CHANGED
|
@@ -3,9 +3,11 @@ const fs = require('fs');
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const { execSync, execFileSync } = require('child_process');
|
|
5
5
|
const { refreshTokens, isExpired, isOAuthToken } = require('./oauth');
|
|
6
|
-
const { saveConfig, loadConfig } = require('./config');
|
|
6
|
+
const { saveConfig, loadConfig, OBOL_DIR } = require('./config');
|
|
7
|
+
const { execAsync, isAllowedUrl } = require('./sanitize');
|
|
7
8
|
|
|
8
9
|
const MAX_EXEC_TIMEOUT = 120;
|
|
10
|
+
const MAX_TOOL_ITERATIONS = 15;
|
|
9
11
|
|
|
10
12
|
const BLOCKED_EXEC_PATTERNS = [
|
|
11
13
|
/\brm\s+(-[a-zA-Z]*f|-[a-zA-Z]*r|--force|--recursive)\b/,
|
|
@@ -19,6 +21,13 @@ const BLOCKED_EXEC_PATTERNS = [
|
|
|
19
21
|
/\$\([^)]*\)/,
|
|
20
22
|
/\bpython[23]?\s+-c\b/, /\bperl\s+-e\b/, /\bruby\s+-e\b/, /\bnode\s+-e\b/,
|
|
21
23
|
/\bcurl\b.*\|\s*(ba)?sh/, /\bwget\b.*\|\s*(ba)?sh/,
|
|
24
|
+
/\benv\b.*\b(sh|bash|zsh)\b/,
|
|
25
|
+
/\bfind\b.*-exec\b/,
|
|
26
|
+
/\bprintf\b.*\|\s*(ba)?sh/,
|
|
27
|
+
/\\x[0-9a-fA-F]{2}/, /\\[0-7]{3}/,
|
|
28
|
+
/\bnc\s+-e\b/, /\bncat\b.*-e\b/,
|
|
29
|
+
/\bmkfifo\b/,
|
|
30
|
+
/>\s*\/dev\/sd/,
|
|
22
31
|
];
|
|
23
32
|
|
|
24
33
|
const SENSITIVE_READ_PATHS = [
|
|
@@ -115,11 +124,10 @@ async function ensureFreshToken(anthropicConfig) {
|
|
|
115
124
|
}
|
|
116
125
|
}
|
|
117
126
|
|
|
118
|
-
function createClaude(anthropicConfig, { personality, memory, userDir, bridgeEnabled }) {
|
|
127
|
+
function createClaude(anthropicConfig, { personality, memory, userDir = OBOL_DIR, bridgeEnabled }) {
|
|
119
128
|
let client = createAnthropicClient(anthropicConfig);
|
|
120
|
-
const useOAuth = !!anthropicConfig.oauth?.accessToken;
|
|
121
129
|
|
|
122
|
-
|
|
130
|
+
let baseSystemPrompt = buildSystemPrompt(personality, userDir, { bridgeEnabled });
|
|
123
131
|
|
|
124
132
|
const histories = new Map();
|
|
125
133
|
const MAX_HISTORY = 50;
|
|
@@ -130,7 +138,7 @@ function createClaude(anthropicConfig, { personality, memory, userDir, bridgeEna
|
|
|
130
138
|
context.userDir = userDir;
|
|
131
139
|
const chatId = context.chatId || 'default';
|
|
132
140
|
|
|
133
|
-
if (
|
|
141
|
+
if (anthropicConfig.oauth?.accessToken) {
|
|
134
142
|
await ensureFreshToken(anthropicConfig);
|
|
135
143
|
if (anthropicConfig._oauthFailed) {
|
|
136
144
|
client = createAnthropicClient(anthropicConfig, { useOAuth: false });
|
|
@@ -203,8 +211,13 @@ Model: Use "sonnet" for most things (chat, simple questions, quick tasks, single
|
|
|
203
211
|
}
|
|
204
212
|
}
|
|
205
213
|
|
|
206
|
-
|
|
207
|
-
|
|
214
|
+
while (history.length >= MAX_HISTORY) {
|
|
215
|
+
history.shift();
|
|
216
|
+
history.shift();
|
|
217
|
+
}
|
|
218
|
+
if (history.length > 0 && history[0].role !== 'user') {
|
|
219
|
+
history.shift();
|
|
220
|
+
}
|
|
208
221
|
|
|
209
222
|
// Add user message with memory context
|
|
210
223
|
const enrichedMessage = memoryContext
|
|
@@ -230,8 +243,21 @@ Model: Use "sonnet" for most things (chat, simple questions, quick tasks, single
|
|
|
230
243
|
tools: tools.length > 0 ? tools : undefined,
|
|
231
244
|
});
|
|
232
245
|
|
|
233
|
-
|
|
246
|
+
let toolIterations = 0;
|
|
234
247
|
while (response.stop_reason === 'tool_use') {
|
|
248
|
+
toolIterations++;
|
|
249
|
+
if (toolIterations > MAX_TOOL_ITERATIONS) {
|
|
250
|
+
history.push({ role: 'assistant', content: response.content });
|
|
251
|
+
history.push({ role: 'user', content: 'You have used too many tool calls. Please provide a final response now based on what you have so far.' });
|
|
252
|
+
response = await client.messages.create({
|
|
253
|
+
model,
|
|
254
|
+
max_tokens: 4096,
|
|
255
|
+
system: systemPrompt,
|
|
256
|
+
messages: history,
|
|
257
|
+
});
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
|
|
235
261
|
const assistantContent = response.content;
|
|
236
262
|
history.push({ role: 'assistant', content: assistantContent });
|
|
237
263
|
|
|
@@ -273,6 +299,7 @@ Model: Use "sonnet" for most things (chat, simple questions, quick tasks, single
|
|
|
273
299
|
const newPersonality = require('./personality').loadPersonality(pDir);
|
|
274
300
|
for (const key of Object.keys(personality)) delete personality[key];
|
|
275
301
|
Object.assign(personality, newPersonality);
|
|
302
|
+
baseSystemPrompt = buildSystemPrompt(personality, userDir, { bridgeEnabled });
|
|
276
303
|
}
|
|
277
304
|
|
|
278
305
|
function clearHistory(chatId) {
|
|
@@ -287,12 +314,51 @@ Model: Use "sonnet" for most things (chat, simple questions, quick tasks, single
|
|
|
287
314
|
}
|
|
288
315
|
|
|
289
316
|
function buildSystemPrompt(personality, userDir, opts = {}) {
|
|
290
|
-
const parts = [
|
|
317
|
+
const parts = [];
|
|
318
|
+
|
|
319
|
+
// Identity core
|
|
320
|
+
parts.push('You are OBOL, a personal AI agent running 24/7 on a server. You have persistent memory, can execute shell commands, deploy websites, and learn over time. You are not a generic chatbot — you are a dedicated agent for one person.');
|
|
321
|
+
|
|
322
|
+
// Personality (from SOUL.md)
|
|
323
|
+
if (personality.soul) {
|
|
324
|
+
parts.push(`\n## Personality\n${personality.soul}`);
|
|
325
|
+
} else {
|
|
326
|
+
parts.push(`\n## Personality\nYou are a fresh instance. Be helpful, direct, and naturally curious. Pay attention to how your owner communicates and adapt. Your personality will develop through conversation and periodic evolution.`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Trait calibration
|
|
330
|
+
if (personality.traits) {
|
|
331
|
+
const t = personality.traits;
|
|
332
|
+
const descriptions = {
|
|
333
|
+
humor: [0, 'suppress all wit', 50, 'balanced wit', 100, 'lean heavily into jokes and playfulness'],
|
|
334
|
+
honesty: [0, 'maximize diplomatic softening', 50, 'balanced honesty', 100, 'lean toward blunt truth'],
|
|
335
|
+
directness: [0, 'elaborate context and preamble', 50, 'balanced', 100, 'get straight to the point'],
|
|
336
|
+
curiosity: [0, 'only answer what is asked', 50, 'balanced', 100, 'proactively explore and ask follow-ups'],
|
|
337
|
+
empathy: [0, 'purely task-focused', 50, 'balanced', 100, 'deeply emotionally attuned'],
|
|
338
|
+
creativity: [0, 'stick to proven patterns', 50, 'balanced', 100, 'favor novel approaches'],
|
|
339
|
+
};
|
|
340
|
+
const lines = Object.entries(t).map(([trait, val]) => {
|
|
341
|
+
const desc = descriptions[trait];
|
|
342
|
+
if (!desc) return null;
|
|
343
|
+
const label = val <= 30 ? desc[1] : val <= 70 ? desc[3] : desc[5];
|
|
344
|
+
return `- ${trait.charAt(0).toUpperCase() + trait.slice(1)}: ${val} — ${label}`;
|
|
345
|
+
}).filter(Boolean);
|
|
346
|
+
parts.push(`\n## Personality Calibration\n\nThese values (0-100) define your behavioral tendencies:\n${lines.join('\n')}\n\nInterpret these as a spectrum: 0 = suppress entirely, 50 = balanced, 100 = lean heavily into it.`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Owner context (from USER.md)
|
|
350
|
+
if (personality.user) {
|
|
351
|
+
parts.push(`\n## About Your Owner\n${personality.user}`);
|
|
352
|
+
} else {
|
|
353
|
+
parts.push(`\n## About Your Owner\nYou don't know anything about your owner yet. Pay attention to everything they share — name, job, interests, preferences, people they mention. Store important details in memory. You'll learn naturally through conversation.`);
|
|
354
|
+
}
|
|
291
355
|
|
|
292
|
-
|
|
293
|
-
if (personality.
|
|
294
|
-
|
|
356
|
+
// Operating instructions (from AGENTS.md — always present via default)
|
|
357
|
+
if (personality.agents) {
|
|
358
|
+
parts.push(`\n## Operating Instructions\n${personality.agents}`);
|
|
359
|
+
}
|
|
295
360
|
|
|
361
|
+
// Workspace discipline
|
|
296
362
|
const workDir = userDir || '~/.obol';
|
|
297
363
|
const userId = userDir ? path.basename(userDir) : null;
|
|
298
364
|
const passPrefix = userId ? `obol/users/${userId}` : 'obol';
|
|
@@ -320,18 +386,18 @@ ${workDir}/
|
|
|
320
386
|
- If unsure where something belongs, ask — don't guess.
|
|
321
387
|
- Run \`/clean\` to audit and fix misplaced files.
|
|
322
388
|
|
|
323
|
-
## Secrets
|
|
389
|
+
## Secrets
|
|
390
|
+
|
|
391
|
+
Use the \`store_secret\`, \`read_secret\`, and \`list_secrets\` tools for all user credential operations.
|
|
392
|
+
These store secrets under the prefix \`${passPrefix}/\` in pass (or JSON fallback).
|
|
324
393
|
|
|
325
|
-
|
|
326
|
-
Example: \`pass insert ${passPrefix}/gmail-key\`
|
|
394
|
+
Users can also manage secrets via Telegram: \`/secret set <key> <value>\` (message auto-deleted), \`/secret list\`, \`/secret remove <key>\`.
|
|
327
395
|
|
|
328
396
|
Shared bot credentials live under \`obol/\` — do NOT touch or re-create these:
|
|
329
397
|
\`obol/anthropic-key\`, \`obol/telegram-token\`, \`obol/supabase-url\`, \`obol/supabase-key\`, \`obol/github-token\`, \`obol/vercel-token\`
|
|
330
|
-
|
|
331
|
-
To check if a secret exists: \`pass show obol/github-token\`
|
|
332
|
-
To list all secrets: \`pass ls\`
|
|
333
398
|
`);
|
|
334
399
|
|
|
400
|
+
// Bridge (conditional)
|
|
335
401
|
if (opts.bridgeEnabled) {
|
|
336
402
|
parts.push(`
|
|
337
403
|
## Bridge (Partner Agent)
|
|
@@ -489,6 +555,40 @@ function buildTools(memory, opts = {}) {
|
|
|
489
555
|
},
|
|
490
556
|
});
|
|
491
557
|
|
|
558
|
+
tools.push({
|
|
559
|
+
name: 'store_secret',
|
|
560
|
+
description: 'Store a secret (API key, password, token) in the per-user encrypted secret store. Use when the user provides credentials for services.',
|
|
561
|
+
input_schema: {
|
|
562
|
+
type: 'object',
|
|
563
|
+
properties: {
|
|
564
|
+
key: { type: 'string', description: 'Secret name (e.g. gmail-password, notion-token)' },
|
|
565
|
+
value: { type: 'string', description: 'Secret value' },
|
|
566
|
+
},
|
|
567
|
+
required: ['key', 'value'],
|
|
568
|
+
},
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
tools.push({
|
|
572
|
+
name: 'read_secret',
|
|
573
|
+
description: 'Read a secret by key from the per-user secret store.',
|
|
574
|
+
input_schema: {
|
|
575
|
+
type: 'object',
|
|
576
|
+
properties: {
|
|
577
|
+
key: { type: 'string', description: 'Secret name to read' },
|
|
578
|
+
},
|
|
579
|
+
required: ['key'],
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
tools.push({
|
|
584
|
+
name: 'list_secrets',
|
|
585
|
+
description: 'List all secret keys stored for this user (keys only, not values).',
|
|
586
|
+
input_schema: {
|
|
587
|
+
type: 'object',
|
|
588
|
+
properties: {},
|
|
589
|
+
},
|
|
590
|
+
});
|
|
591
|
+
|
|
492
592
|
if (opts.bridgeEnabled) {
|
|
493
593
|
const { buildBridgeTool, buildBridgeTellTool } = require('./bridge');
|
|
494
594
|
tools.push(buildBridgeTool());
|
|
@@ -499,7 +599,7 @@ function buildTools(memory, opts = {}) {
|
|
|
499
599
|
}
|
|
500
600
|
|
|
501
601
|
function resolveUserPath(inputPath, userDir) {
|
|
502
|
-
if (!userDir)
|
|
602
|
+
if (!userDir) throw new Error('userDir is required for path resolution');
|
|
503
603
|
const resolved = path.isAbsolute(inputPath)
|
|
504
604
|
? path.resolve(inputPath)
|
|
505
605
|
: path.resolve(userDir, inputPath);
|
|
@@ -555,11 +655,10 @@ async function executeToolCall(toolUse, memory, context = {}) {
|
|
|
555
655
|
}
|
|
556
656
|
const timeout = Math.min(input.timeout || 30, MAX_EXEC_TIMEOUT) * 1000;
|
|
557
657
|
const realHome = process.env.HOME || '/root';
|
|
558
|
-
const output =
|
|
658
|
+
const output = await execAsync(input.command, {
|
|
559
659
|
encoding: 'utf-8',
|
|
560
660
|
timeout,
|
|
561
661
|
maxBuffer: 1024 * 1024,
|
|
562
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
563
662
|
cwd: userDir || undefined,
|
|
564
663
|
env: userDir ? {
|
|
565
664
|
...process.env,
|
|
@@ -609,6 +708,7 @@ async function executeToolCall(toolUse, memory, context = {}) {
|
|
|
609
708
|
if (!bg || !telegramCtx) return 'Background tasks not available in this context.';
|
|
610
709
|
if (!claudeInstance) return 'Background tasks not available.';
|
|
611
710
|
const taskId = bg.spawn(claudeInstance, input.task, telegramCtx, memory);
|
|
711
|
+
if (taskId === null) return 'Too many background tasks running. Wait for one to finish.';
|
|
612
712
|
return `Background task #${taskId} spawned. It will send progress updates and the final result to the chat.`;
|
|
613
713
|
}
|
|
614
714
|
|
|
@@ -647,6 +747,7 @@ async function executeToolCall(toolUse, memory, context = {}) {
|
|
|
647
747
|
}
|
|
648
748
|
|
|
649
749
|
case 'web_fetch': {
|
|
750
|
+
if (!isAllowedUrl(input.url)) return 'Blocked: URL points to a private/internal address.';
|
|
650
751
|
const jinaUrl = `https://r.jina.ai/${input.url}`;
|
|
651
752
|
const res = await fetch(jinaUrl, {
|
|
652
753
|
headers: { 'Accept': 'text/markdown' },
|
|
@@ -670,6 +771,26 @@ async function executeToolCall(toolUse, memory, context = {}) {
|
|
|
670
771
|
return `Written: ${filePath}`;
|
|
671
772
|
}
|
|
672
773
|
|
|
774
|
+
case 'store_secret': {
|
|
775
|
+
const credentials = require('./credentials');
|
|
776
|
+
credentials.storeSecret(context.userId, input.key, input.value);
|
|
777
|
+
return `Stored secret: ${input.key}`;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
case 'read_secret': {
|
|
781
|
+
const credentials = require('./credentials');
|
|
782
|
+
const val = credentials.readSecret(context.userId, input.key);
|
|
783
|
+
if (val === null) return `Secret not found: ${input.key}`;
|
|
784
|
+
return val;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
case 'list_secrets': {
|
|
788
|
+
const credentials = require('./credentials');
|
|
789
|
+
const keys = credentials.listSecrets(context.userId);
|
|
790
|
+
if (keys.length === 0) return 'No secrets stored.';
|
|
791
|
+
return keys.join('\n');
|
|
792
|
+
}
|
|
793
|
+
|
|
673
794
|
case 'bridge_ask': {
|
|
674
795
|
const { bridgeAsk } = require('./bridge');
|
|
675
796
|
return await bridgeAsk(input.question, context.userId, context.config, context._notifyFn, input.partner_id);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const { execFileSync } = require('child_process');
|
|
3
|
+
const inquirer = require('inquirer');
|
|
4
|
+
const { OBOL_DIR } = require('../config');
|
|
5
|
+
const { hasPassStore } = require('../credentials');
|
|
6
|
+
const { stop } = require('./stop');
|
|
7
|
+
|
|
8
|
+
async function deleteAll() {
|
|
9
|
+
const passAvailable = hasPassStore();
|
|
10
|
+
const obolExists = fs.existsSync(OBOL_DIR);
|
|
11
|
+
|
|
12
|
+
if (!obolExists && !passAvailable) {
|
|
13
|
+
console.log('🪙 Nothing to delete — no OBOL data found');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
console.log('\n⚠️ This will permanently delete ALL OBOL data:\n');
|
|
18
|
+
if (obolExists) console.log(` • ${OBOL_DIR}/`);
|
|
19
|
+
if (passAvailable) console.log(' • pass entries under obol/');
|
|
20
|
+
console.log();
|
|
21
|
+
|
|
22
|
+
const { confirm } = await inquirer.prompt({
|
|
23
|
+
type: 'confirm',
|
|
24
|
+
name: 'confirm',
|
|
25
|
+
message: 'This will permanently delete ALL OBOL data. Continue?',
|
|
26
|
+
default: false,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (!confirm) {
|
|
30
|
+
console.log('🪙 Aborted');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { typed } = await inquirer.prompt({
|
|
35
|
+
type: 'input',
|
|
36
|
+
name: 'typed',
|
|
37
|
+
message: 'Type DELETE to confirm:',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (typed !== 'DELETE') {
|
|
41
|
+
console.log('🪙 Aborted');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
await stop();
|
|
46
|
+
|
|
47
|
+
if (passAvailable) {
|
|
48
|
+
try {
|
|
49
|
+
execFileSync('pass', ['rm', '-r', '--force', 'obol/'], {
|
|
50
|
+
encoding: 'utf-8',
|
|
51
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
52
|
+
});
|
|
53
|
+
} catch {}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (obolExists) {
|
|
57
|
+
fs.rmSync(OBOL_DIR, { recursive: true, force: true });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.log('🪙 All OBOL data deleted — run `obol init` to start fresh');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { delete: deleteAll };
|
package/src/config.js
CHANGED
|
@@ -20,8 +20,8 @@ function resolvePassValues(obj) {
|
|
|
20
20
|
if (typeof result[key] === 'string' && result[key].startsWith('pass:')) {
|
|
21
21
|
const passKey = result[key].slice(5);
|
|
22
22
|
try {
|
|
23
|
-
const {
|
|
24
|
-
result[key] =
|
|
23
|
+
const { execFileSync } = require('child_process');
|
|
24
|
+
result[key] = execFileSync('pass', ['show', passKey], { encoding: 'utf-8' }).trim();
|
|
25
25
|
} catch (e) {
|
|
26
26
|
const reason = e.message?.includes('not found') ? 'key not found' : 'pass not installed or unavailable';
|
|
27
27
|
console.error(`[config] Failed to resolve ${passKey} — ${reason}`);
|
|
@@ -86,6 +86,16 @@ function ensureUserDir(userId) {
|
|
|
86
86
|
for (const sub of ['personality', 'scripts', 'tests', 'commands', 'apps', 'logs', 'assets']) {
|
|
87
87
|
fs.mkdirSync(path.join(dir, sub), { recursive: true });
|
|
88
88
|
}
|
|
89
|
+
const defaultAgents = path.join(__dirname, 'defaults', 'AGENTS.md');
|
|
90
|
+
const targetAgents = path.join(dir, 'personality', 'AGENTS.md');
|
|
91
|
+
if (fs.existsSync(defaultAgents) && !fs.existsSync(targetAgents)) {
|
|
92
|
+
fs.copyFileSync(defaultAgents, targetAgents);
|
|
93
|
+
}
|
|
94
|
+
const defaultTraits = path.join(__dirname, 'defaults', 'traits.json');
|
|
95
|
+
const targetTraits = path.join(dir, 'personality', 'traits.json');
|
|
96
|
+
if (fs.existsSync(defaultTraits) && !fs.existsSync(targetTraits)) {
|
|
97
|
+
fs.copyFileSync(defaultTraits, targetTraits);
|
|
98
|
+
}
|
|
89
99
|
return dir;
|
|
90
100
|
}
|
|
91
101
|
|
|
@@ -103,6 +113,7 @@ module.exports = {
|
|
|
103
113
|
PID_FILE,
|
|
104
114
|
LOG_FILE,
|
|
105
115
|
getConfigDir,
|
|
116
|
+
resolvePassValues,
|
|
106
117
|
loadConfig,
|
|
107
118
|
saveConfig,
|
|
108
119
|
getUserDir,
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const { execFileSync } = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { getUserDir } = require('./config');
|
|
5
|
+
|
|
6
|
+
const KEY_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}$/;
|
|
7
|
+
|
|
8
|
+
function validateKey(key) {
|
|
9
|
+
if (!key || typeof key !== 'string') throw new Error('Key is required');
|
|
10
|
+
if (!KEY_PATTERN.test(key)) {
|
|
11
|
+
throw new Error('Key must be 1-64 chars: letters, numbers, hyphens, dots, underscores');
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function hasPassStore() {
|
|
16
|
+
try {
|
|
17
|
+
execFileSync('which', ['pass'], { encoding: 'utf-8', stdio: 'pipe' });
|
|
18
|
+
return true;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function passPrefix(userId) {
|
|
25
|
+
return `obol/users/${userId}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function secretsJsonPath(userId) {
|
|
29
|
+
const dir = getUserDir(userId);
|
|
30
|
+
return path.join(dir, 'secrets.json');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function loadSecretsJson(userId) {
|
|
34
|
+
const p = secretsJsonPath(userId);
|
|
35
|
+
if (!fs.existsSync(p)) return {};
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
38
|
+
} catch {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function saveSecretsJson(userId, data) {
|
|
44
|
+
const p = secretsJsonPath(userId);
|
|
45
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
46
|
+
fs.writeFileSync(p, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function storeSecret(userId, key, value) {
|
|
50
|
+
validateKey(key);
|
|
51
|
+
if (!value || typeof value !== 'string') throw new Error('Value is required');
|
|
52
|
+
|
|
53
|
+
if (hasPassStore()) {
|
|
54
|
+
const passKey = `${passPrefix(userId)}/${key}`;
|
|
55
|
+
execFileSync('pass', ['insert', '--force', '--multiline', passKey], {
|
|
56
|
+
input: value,
|
|
57
|
+
encoding: 'utf-8',
|
|
58
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const secrets = loadSecretsJson(userId);
|
|
64
|
+
secrets[key] = value;
|
|
65
|
+
saveSecretsJson(userId, secrets);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readSecret(userId, key) {
|
|
69
|
+
validateKey(key);
|
|
70
|
+
|
|
71
|
+
if (hasPassStore()) {
|
|
72
|
+
try {
|
|
73
|
+
const passKey = `${passPrefix(userId)}/${key}`;
|
|
74
|
+
return execFileSync('pass', ['show', passKey], {
|
|
75
|
+
encoding: 'utf-8',
|
|
76
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
77
|
+
}).trim();
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const secrets = loadSecretsJson(userId);
|
|
84
|
+
return secrets[key] || null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function removeSecret(userId, key) {
|
|
88
|
+
validateKey(key);
|
|
89
|
+
|
|
90
|
+
if (hasPassStore()) {
|
|
91
|
+
try {
|
|
92
|
+
const passKey = `${passPrefix(userId)}/${key}`;
|
|
93
|
+
execFileSync('pass', ['rm', '--force', passKey], {
|
|
94
|
+
encoding: 'utf-8',
|
|
95
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
96
|
+
});
|
|
97
|
+
} catch {}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const secrets = loadSecretsJson(userId);
|
|
102
|
+
delete secrets[key];
|
|
103
|
+
saveSecretsJson(userId, secrets);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function listSecrets(userId) {
|
|
107
|
+
if (hasPassStore()) {
|
|
108
|
+
try {
|
|
109
|
+
const prefix = passPrefix(userId);
|
|
110
|
+
const output = execFileSync('pass', ['ls', prefix], {
|
|
111
|
+
encoding: 'utf-8',
|
|
112
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
113
|
+
});
|
|
114
|
+
return output.split('\n')
|
|
115
|
+
.map(line => line.replace(/[│├└──\s]/g, '').replace(/\.gpg$/, '').trim())
|
|
116
|
+
.filter(Boolean);
|
|
117
|
+
} catch {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const secrets = loadSecretsJson(userId);
|
|
123
|
+
return Object.keys(secrets);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = { storeSecret, readSecret, removeSecret, listSecrets, hasPassStore, validateKey };
|