clementine-agent 1.1.14 → 1.1.16
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/dist/agent/agent-manager.d.ts +1 -2
- package/dist/agent/agent-manager.js +5 -10
- package/dist/agent/assistant.js +2 -2
- package/dist/cli/chat.js +2 -2
- package/dist/cli/dashboard.js +8 -14
- package/dist/config/effective-config.js +2 -20
- package/dist/config/env-parser.d.ts +27 -0
- package/dist/config/env-parser.js +47 -0
- package/dist/config.d.ts +2 -2
- package/dist/config.js +8 -22
- package/dist/gateway/router.js +2 -2
- package/dist/index.js +0 -1
- package/dist/memory/graph-store.d.ts +24 -0
- package/dist/memory/graph-store.js +177 -19
- package/dist/tools/shared.d.ts +0 -1
- package/dist/tools/shared.js +2 -18
- package/dist/tools/team-tools.js +4 -16
- package/dist/vault-migrations/0005-create-clementine-json.js +2 -17
- package/package.json +1 -1
- package/dist/agent/profiles.d.ts +0 -22
- package/dist/agent/profiles.js +0 -91
|
@@ -38,10 +38,9 @@ export interface AgentCreateConfig {
|
|
|
38
38
|
}
|
|
39
39
|
export declare class AgentManager {
|
|
40
40
|
private agentsDir;
|
|
41
|
-
private legacyManager;
|
|
42
41
|
private cache;
|
|
43
42
|
private cacheTime;
|
|
44
|
-
constructor(agentsDir: string
|
|
43
|
+
constructor(agentsDir: string);
|
|
45
44
|
private refreshIfStale;
|
|
46
45
|
private loadAgentFile;
|
|
47
46
|
get(slug: string): AgentProfile | null;
|
|
@@ -15,7 +15,10 @@ import fs from 'node:fs';
|
|
|
15
15
|
import path from 'node:path';
|
|
16
16
|
import matter from 'gray-matter';
|
|
17
17
|
import { randomBytes } from 'node:crypto';
|
|
18
|
-
|
|
18
|
+
// Phase 14 cleanup: legacy ProfileManager (vault/00-System/profiles/*.md)
|
|
19
|
+
// removed — that directory has been empty for a long time and the new format
|
|
20
|
+
// (vault/00-System/agents/<slug>/agent.md) supersedes it. Constructor no
|
|
21
|
+
// longer takes a legacyProfilesDir argument.
|
|
19
22
|
import { getScaffoldForRole } from './role-scaffolds.js';
|
|
20
23
|
import { writeGoalForOwner } from '../tools/shared.js';
|
|
21
24
|
// ── Keychain helpers for agent secrets ────────────────────────────────
|
|
@@ -39,12 +42,10 @@ function deleteAgentSecret(slug, key) {
|
|
|
39
42
|
const CACHE_TTL_MS = 60_000;
|
|
40
43
|
export class AgentManager {
|
|
41
44
|
agentsDir;
|
|
42
|
-
legacyManager;
|
|
43
45
|
cache = new Map();
|
|
44
46
|
cacheTime = 0;
|
|
45
|
-
constructor(agentsDir
|
|
47
|
+
constructor(agentsDir) {
|
|
46
48
|
this.agentsDir = agentsDir;
|
|
47
|
-
this.legacyManager = new ProfileManager(legacyProfilesDir);
|
|
48
49
|
}
|
|
49
50
|
refreshIfStale() {
|
|
50
51
|
const now = Date.now();
|
|
@@ -76,12 +77,6 @@ export class AgentManager {
|
|
|
76
77
|
// agents dir not readable
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
|
-
// 2. Load legacy profiles (only for slugs not already loaded)
|
|
80
|
-
for (const legacy of this.legacyManager.listAll()) {
|
|
81
|
-
if (!profiles.has(legacy.slug)) {
|
|
82
|
-
profiles.set(legacy.slug, legacy);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
80
|
this.cache = profiles;
|
|
86
81
|
this.cacheTime = now;
|
|
87
82
|
}
|
package/dist/agent/assistant.js
CHANGED
|
@@ -13,7 +13,7 @@ import fs from 'node:fs';
|
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { query as rawQuery, listSubagents, getSubagentMessages, SYSTEM_PROMPT_DYNAMIC_BOUNDARY, } from '@anthropic-ai/claude-agent-sdk';
|
|
15
15
|
import pino from 'pino';
|
|
16
|
-
import { BASE_DIR, PKG_DIR, VAULT_DIR, DAILY_NOTES_DIR, SOUL_FILE, AGENTS_FILE, MEMORY_FILE,
|
|
16
|
+
import { BASE_DIR, PKG_DIR, VAULT_DIR, DAILY_NOTES_DIR, SOUL_FILE, AGENTS_FILE, MEMORY_FILE, AGENTS_DIR, ASSISTANT_NAME, OWNER_NAME, MODEL, MODELS, HEARTBEAT_MAX_TURNS, SEARCH_CONTEXT_LIMIT, SEARCH_RECENCY_LIMIT, SYSTEM_PROMPT_MAX_CONTEXT_CHARS, SESSION_EXCHANGE_HISTORY_SIZE, SESSION_EXCHANGE_MAX_CHARS, INJECTED_CONTEXT_MAX_CHARS, UNLEASHED_PHASE_TURNS, UNLEASHED_DEFAULT_MAX_HOURS, UNLEASHED_MAX_PHASES, PROJECTS_META_FILE, CRON_PROGRESS_DIR, CRON_REFLECTIONS_DIR, HANDOFFS_DIR, BUDGET, TASK_BUDGET_TOKENS, ENABLE_1M_CONTEXT, IDENTITY_FILE, CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY as CONFIG_ANTHROPIC_API_KEY, } from '../config.js';
|
|
17
17
|
import { DEFAULT_CHANNEL_CAPABILITIES } from '../types.js';
|
|
18
18
|
import { enforceToolPermissions, getSecurityPrompt, getHeartbeatSecurityPrompt, getCronSecurityPrompt, getHeartbeatDisallowedTools, logToolUse, setProfileTier, setProfileAllowedTools, setAgentDir, setSendPolicy, setInteractionSource, logAuditJsonl, } from './hooks.js';
|
|
19
19
|
import { scanner } from '../security/scanner.js';
|
|
@@ -746,7 +746,7 @@ export class PersonalAssistant {
|
|
|
746
746
|
/** Hot correction buffer — explicit behavioral corrections applied before nightly SI. */
|
|
747
747
|
hotCorrections = [];
|
|
748
748
|
constructor() {
|
|
749
|
-
this.profileManager = new AgentManager(AGENTS_DIR
|
|
749
|
+
this.profileManager = new AgentManager(AGENTS_DIR);
|
|
750
750
|
this.promptCache = new PromptCache();
|
|
751
751
|
this.initPromptWatchers();
|
|
752
752
|
this.loadSessions();
|
package/dist/cli/chat.js
CHANGED
|
@@ -74,9 +74,9 @@ export async function cmdChat(opts) {
|
|
|
74
74
|
}
|
|
75
75
|
// Apply initial profile override
|
|
76
76
|
if (opts.profile) {
|
|
77
|
-
const {
|
|
77
|
+
const { AGENTS_DIR } = await import('../config.js');
|
|
78
78
|
const { AgentManager } = await import('../agent/agent-manager.js');
|
|
79
|
-
const pm = new AgentManager(AGENTS_DIR
|
|
79
|
+
const pm = new AgentManager(AGENTS_DIR);
|
|
80
80
|
const profile = pm.get(opts.profile);
|
|
81
81
|
if (profile) {
|
|
82
82
|
gateway.setSessionProfile(sessionKey, opts.profile);
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -1633,8 +1633,7 @@ export async function cmdDashboard(opts) {
|
|
|
1633
1633
|
}
|
|
1634
1634
|
result.projects = { projects: cachedProjects ?? [] };
|
|
1635
1635
|
try {
|
|
1636
|
-
const
|
|
1637
|
-
const mgr = new AgentManager(AGENTS_DIR, profilesDir);
|
|
1636
|
+
const mgr = new AgentManager(AGENTS_DIR);
|
|
1638
1637
|
const allAgents = mgr.listAll();
|
|
1639
1638
|
let botStatuses = {};
|
|
1640
1639
|
try {
|
|
@@ -1964,8 +1963,7 @@ export async function cmdDashboard(opts) {
|
|
|
1964
1963
|
//
|
|
1965
1964
|
try {
|
|
1966
1965
|
const agDir = AGENTS_DIR;
|
|
1967
|
-
const
|
|
1968
|
-
const mgr = new AgentManager(agDir, profilesDir);
|
|
1966
|
+
const mgr = new AgentManager(agDir);
|
|
1969
1967
|
const allAgents = mgr.listAll();
|
|
1970
1968
|
// Bot statuses from disk
|
|
1971
1969
|
let botStatuses = {};
|
|
@@ -4278,12 +4276,10 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
4278
4276
|
});
|
|
4279
4277
|
// ── Profile routes ─────────────────────────────────────────────────
|
|
4280
4278
|
app.get('/api/profiles', gwHandler(async (gw, _req, res) => {
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
}
|
|
4286
|
-
const pm = new AgentManager(AGENTS_DIR, profilesDir);
|
|
4279
|
+
// Phase 14: previously checked the legacy profilesDir; now that the
|
|
4280
|
+
// legacy fallback is gone, AgentManager.listAll() returns [] when
|
|
4281
|
+
// AGENTS_DIR is missing or empty. No separate guard needed.
|
|
4282
|
+
const pm = new AgentManager(AGENTS_DIR);
|
|
4287
4283
|
const profiles = pm.listAll().map(p => ({
|
|
4288
4284
|
slug: p.slug,
|
|
4289
4285
|
name: p.name,
|
|
@@ -5343,8 +5339,7 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
5343
5339
|
// ── Team API endpoints ──────────────────────────────────────────────
|
|
5344
5340
|
app.get('/api/team/agents', async (_req, res) => {
|
|
5345
5341
|
try {
|
|
5346
|
-
const
|
|
5347
|
-
const mgr = new AgentManager(AGENTS_DIR, profilesDir);
|
|
5342
|
+
const mgr = new AgentManager(AGENTS_DIR);
|
|
5348
5343
|
const agents = mgr.listAll();
|
|
5349
5344
|
res.json(agents.map(a => ({
|
|
5350
5345
|
slug: a.slug,
|
|
@@ -5440,8 +5435,7 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
5440
5435
|
app.get('/api/office', async (_req, res) => {
|
|
5441
5436
|
try {
|
|
5442
5437
|
const data = await cachedAsync('office', 10_000, async () => {
|
|
5443
|
-
const
|
|
5444
|
-
const mgr = new AgentManager(AGENTS_DIR, profilesDir);
|
|
5438
|
+
const mgr = new AgentManager(AGENTS_DIR);
|
|
5445
5439
|
const allAgents = mgr.listAll();
|
|
5446
5440
|
// ── Bot statuses ──
|
|
5447
5441
|
let botStatuses = {};
|
|
@@ -59,11 +59,9 @@ const SPECS = [
|
|
|
59
59
|
// ── Keychain-ref resolution ──────────────────────────────────────────
|
|
60
60
|
// Mirrors config.ts's lazy/cached resolver but kept independent so this
|
|
61
61
|
// module stays a pure utility (no shared mutable state with the daemon).
|
|
62
|
+
import { parseEnvText, shellEscape } from './env-parser.js';
|
|
62
63
|
const KEYCHAIN_REF_PREFIX = 'keychain:'; // pragma: allowlist secret
|
|
63
64
|
const refCache = new Map();
|
|
64
|
-
function shellEscape(s) {
|
|
65
|
-
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
66
|
-
}
|
|
67
65
|
/** Hard cap per shell call — see config.ts for rationale. */
|
|
68
66
|
const KEYCHAIN_TIMEOUT_MS = Math.max(500, parseInt(process.env.CLEMENTINE_KEYCHAIN_TIMEOUT_MS ?? '3000', 10) || 3000);
|
|
69
67
|
function resolveRef(stub) {
|
|
@@ -86,23 +84,7 @@ function readEnvFile(baseDir) {
|
|
|
86
84
|
const envPath = path.join(baseDir, '.env');
|
|
87
85
|
if (!existsSync(envPath))
|
|
88
86
|
return {};
|
|
89
|
-
|
|
90
|
-
for (const line of readFileSync(envPath, 'utf-8').split('\n')) {
|
|
91
|
-
const trimmed = line.trim();
|
|
92
|
-
if (!trimmed || trimmed.startsWith('#'))
|
|
93
|
-
continue;
|
|
94
|
-
const eqIndex = trimmed.indexOf('=');
|
|
95
|
-
if (eqIndex === -1)
|
|
96
|
-
continue;
|
|
97
|
-
const key = trimmed.slice(0, eqIndex);
|
|
98
|
-
let value = trimmed.slice(eqIndex + 1);
|
|
99
|
-
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
100
|
-
(value.startsWith("'") && value.endsWith("'"))) {
|
|
101
|
-
value = value.slice(1, -1);
|
|
102
|
-
}
|
|
103
|
-
result[key] = value;
|
|
104
|
-
}
|
|
105
|
-
return result;
|
|
87
|
+
return parseEnvText(readFileSync(envPath, 'utf-8'));
|
|
106
88
|
}
|
|
107
89
|
function getJsonValue(json, dottedPath) {
|
|
108
90
|
const parts = dottedPath.split('.');
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared parser for `.env`-style files.
|
|
3
|
+
*
|
|
4
|
+
* Phase 14 cleanup: this logic was previously duplicated in 4+ places
|
|
5
|
+
* (src/config.ts, src/tools/shared.ts, src/config/effective-config.ts,
|
|
6
|
+
* src/vault-migrations/0005-create-clementine-json.ts). All implementations
|
|
7
|
+
* were identical line-for-line; consolidating here avoids future drift.
|
|
8
|
+
*
|
|
9
|
+
* Each caller still wraps this with its own file-read since the path
|
|
10
|
+
* varies (BASE_DIR-relative vs explicit baseDir param vs lazy/cached).
|
|
11
|
+
*
|
|
12
|
+
* Format:
|
|
13
|
+
* KEY=value — bare value
|
|
14
|
+
* KEY="value" — double-quoted; quotes stripped
|
|
15
|
+
* KEY='value' — single-quoted; quotes stripped
|
|
16
|
+
* # comment — ignored
|
|
17
|
+
* <blank line> — ignored
|
|
18
|
+
* malformed lines — silently skipped (no `=` separator)
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseEnvText(text: string): Record<string, string>;
|
|
21
|
+
/**
|
|
22
|
+
* POSIX shell single-quote escape. Used by every keychain shell-out site.
|
|
23
|
+
* Phase 14 cleanup: was duplicated in src/config.ts and
|
|
24
|
+
* src/config/effective-config.ts.
|
|
25
|
+
*/
|
|
26
|
+
export declare function shellEscape(s: string): string;
|
|
27
|
+
//# sourceMappingURL=env-parser.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared parser for `.env`-style files.
|
|
3
|
+
*
|
|
4
|
+
* Phase 14 cleanup: this logic was previously duplicated in 4+ places
|
|
5
|
+
* (src/config.ts, src/tools/shared.ts, src/config/effective-config.ts,
|
|
6
|
+
* src/vault-migrations/0005-create-clementine-json.ts). All implementations
|
|
7
|
+
* were identical line-for-line; consolidating here avoids future drift.
|
|
8
|
+
*
|
|
9
|
+
* Each caller still wraps this with its own file-read since the path
|
|
10
|
+
* varies (BASE_DIR-relative vs explicit baseDir param vs lazy/cached).
|
|
11
|
+
*
|
|
12
|
+
* Format:
|
|
13
|
+
* KEY=value — bare value
|
|
14
|
+
* KEY="value" — double-quoted; quotes stripped
|
|
15
|
+
* KEY='value' — single-quoted; quotes stripped
|
|
16
|
+
* # comment — ignored
|
|
17
|
+
* <blank line> — ignored
|
|
18
|
+
* malformed lines — silently skipped (no `=` separator)
|
|
19
|
+
*/
|
|
20
|
+
export function parseEnvText(text) {
|
|
21
|
+
const result = {};
|
|
22
|
+
for (const line of text.split('\n')) {
|
|
23
|
+
const trimmed = line.trim();
|
|
24
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
25
|
+
continue;
|
|
26
|
+
const eqIndex = trimmed.indexOf('=');
|
|
27
|
+
if (eqIndex === -1)
|
|
28
|
+
continue;
|
|
29
|
+
const key = trimmed.slice(0, eqIndex);
|
|
30
|
+
let value = trimmed.slice(eqIndex + 1);
|
|
31
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
32
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
33
|
+
value = value.slice(1, -1);
|
|
34
|
+
}
|
|
35
|
+
result[key] = value;
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* POSIX shell single-quote escape. Used by every keychain shell-out site.
|
|
41
|
+
* Phase 14 cleanup: was duplicated in src/config.ts and
|
|
42
|
+
* src/config/effective-config.ts.
|
|
43
|
+
*/
|
|
44
|
+
export function shellEscape(s) {
|
|
45
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=env-parser.js.map
|
package/dist/config.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { Models } from './types.js';
|
|
|
10
10
|
export declare const PKG_DIR: string;
|
|
11
11
|
/** Data home — user data, vault, .env, logs, sessions. */
|
|
12
12
|
export declare const BASE_DIR: string;
|
|
13
|
+
import { shellEscape as _shellEscape } from './config/env-parser.js';
|
|
13
14
|
/** Merged view of process.env overlaid with .env. Use for classifyIntegrations / summarizeIntegrationStatus. */
|
|
14
15
|
export declare function envSnapshot(): Record<string, string | undefined>;
|
|
15
16
|
/** Test-only: clear the keychain ref cache so re-resolution can be tested. */
|
|
@@ -30,7 +31,6 @@ export declare const TOPICS_DIR: string;
|
|
|
30
31
|
export declare const TASKS_DIR: string;
|
|
31
32
|
export declare const TEMPLATES_DIR: string;
|
|
32
33
|
export declare const INBOX_DIR: string;
|
|
33
|
-
export declare const PROFILES_DIR: string;
|
|
34
34
|
export declare const AGENTS_DIR: string;
|
|
35
35
|
export declare const SOUL_FILE: string;
|
|
36
36
|
export declare const AGENTS_FILE: string;
|
|
@@ -47,7 +47,7 @@ export declare const IDENTITY_FILE: string;
|
|
|
47
47
|
export declare const ASSISTANT_NAME: string;
|
|
48
48
|
export declare const ASSISTANT_NICKNAME: string;
|
|
49
49
|
export declare const OWNER_NAME: string;
|
|
50
|
-
export declare
|
|
50
|
+
export declare const shellEscape: typeof _shellEscape;
|
|
51
51
|
export declare const MODELS: Models;
|
|
52
52
|
export declare const BUDGET: {
|
|
53
53
|
heartbeat: number;
|
package/dist/config.js
CHANGED
|
@@ -19,28 +19,12 @@ export const PKG_DIR = path.resolve(__dirname, '..');
|
|
|
19
19
|
/** Data home — user data, vault, .env, logs, sessions. */
|
|
20
20
|
export const BASE_DIR = process.env.CLEMENTINE_HOME || path.join(os.homedir(), '.clementine');
|
|
21
21
|
// ── .env parser (never sets process.env) ────────────────────────────
|
|
22
|
+
import { parseEnvText, shellEscape as _shellEscape } from './config/env-parser.js';
|
|
22
23
|
function readEnvFile() {
|
|
23
24
|
const envPath = path.join(BASE_DIR, '.env');
|
|
24
25
|
if (!existsSync(envPath))
|
|
25
26
|
return {};
|
|
26
|
-
|
|
27
|
-
for (const line of readFileSync(envPath, 'utf-8').split('\n')) {
|
|
28
|
-
const trimmed = line.trim();
|
|
29
|
-
if (!trimmed || trimmed.startsWith('#'))
|
|
30
|
-
continue;
|
|
31
|
-
const eqIndex = trimmed.indexOf('=');
|
|
32
|
-
if (eqIndex === -1)
|
|
33
|
-
continue;
|
|
34
|
-
const key = trimmed.slice(0, eqIndex);
|
|
35
|
-
let value = trimmed.slice(eqIndex + 1);
|
|
36
|
-
// Strip surrounding quotes
|
|
37
|
-
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
38
|
-
(value.startsWith("'") && value.endsWith("'"))) {
|
|
39
|
-
value = value.slice(1, -1);
|
|
40
|
-
}
|
|
41
|
-
result[key] = value;
|
|
42
|
-
}
|
|
43
|
-
return result;
|
|
27
|
+
return parseEnvText(readFileSync(envPath, 'utf-8'));
|
|
44
28
|
}
|
|
45
29
|
const env = readEnvFile();
|
|
46
30
|
// ── Keychain-ref resolution (lazy, cached) ──────────────────────────
|
|
@@ -141,7 +125,9 @@ export const TOPICS_DIR = path.join(VAULT_DIR, '04-Topics');
|
|
|
141
125
|
export const TASKS_DIR = path.join(VAULT_DIR, '05-Tasks');
|
|
142
126
|
export const TEMPLATES_DIR = path.join(VAULT_DIR, '06-Templates');
|
|
143
127
|
export const INBOX_DIR = path.join(VAULT_DIR, '07-Inbox');
|
|
144
|
-
|
|
128
|
+
// PROFILES_DIR (vault/00-System/profiles/) removed in Phase 14 cleanup —
|
|
129
|
+
// the legacy profile format hasn't been used in production for a long time.
|
|
130
|
+
// AGENTS_DIR is the canonical home for agent definitions.
|
|
145
131
|
export const AGENTS_DIR = path.join(SYSTEM_DIR, 'agents');
|
|
146
132
|
export const SOUL_FILE = path.join(SYSTEM_DIR, 'SOUL.md');
|
|
147
133
|
export const AGENTS_FILE = path.join(SYSTEM_DIR, 'AGENTS.md');
|
|
@@ -171,9 +157,9 @@ export const ASSISTANT_NAME = getEnvOrJson('ASSISTANT_NAME', json.assistantName,
|
|
|
171
157
|
export const ASSISTANT_NICKNAME = getEnv('ASSISTANT_NICKNAME', 'Clemmy');
|
|
172
158
|
export const OWNER_NAME = getEnvOrJson('OWNER_NAME', json.ownerName, '');
|
|
173
159
|
// ── Secrets (with macOS Keychain fallback) ───────────────────────────
|
|
174
|
-
export
|
|
175
|
-
|
|
176
|
-
|
|
160
|
+
// Re-export shellEscape from the shared helper so existing call sites
|
|
161
|
+
// (config.ts internal uses + downstream importers) keep working.
|
|
162
|
+
export const shellEscape = _shellEscape;
|
|
177
163
|
function getSecret(envKey, keychainService) {
|
|
178
164
|
// Resolve keychain refs from .env in place so secrets stored as stubs
|
|
179
165
|
// come back as their real values (same blind spot as getEnv had pre-fix).
|
package/dist/gateway/router.js
CHANGED
|
@@ -10,7 +10,7 @@ import pino from 'pino';
|
|
|
10
10
|
import { PersonalAssistant } from '../agent/assistant.js';
|
|
11
11
|
import { runWithTrace, logAuditJsonl } from '../agent/hooks.js';
|
|
12
12
|
import { SelfImproveLoop } from '../agent/self-improve.js';
|
|
13
|
-
import { MODELS,
|
|
13
|
+
import { MODELS, AGENTS_DIR, TEAM_COMMS_LOG, BASE_DIR, SEEN_CHANNELS_FILE } from '../config.js';
|
|
14
14
|
import { scanner } from '../security/scanner.js';
|
|
15
15
|
import { lanes } from './lanes.js';
|
|
16
16
|
import { AgentManager } from '../agent/agent-manager.js';
|
|
@@ -338,7 +338,7 @@ export class Gateway {
|
|
|
338
338
|
// ── Team system accessors ──────────────────────────────────────────
|
|
339
339
|
getAgentManager() {
|
|
340
340
|
if (!this._agentManager) {
|
|
341
|
-
this._agentManager = new AgentManager(AGENTS_DIR
|
|
341
|
+
this._agentManager = new AgentManager(AGENTS_DIR);
|
|
342
342
|
}
|
|
343
343
|
return this._agentManager;
|
|
344
344
|
}
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,14 @@
|
|
|
16
16
|
* returns false and all graph features are silently skipped.
|
|
17
17
|
*/
|
|
18
18
|
import type { EntityNode, EntityRef, GraphSyncStats, PathResult, RelationshipTriplet, TraversalResult } from '../types.js';
|
|
19
|
+
/**
|
|
20
|
+
* Phase 13 — extract structured info from arbitrary error objects so log
|
|
21
|
+
* entries always carry SOMETHING useful even when err.message is empty.
|
|
22
|
+
* Node socket errors have .code, .errno, .syscall, .address, .port that
|
|
23
|
+
* tell us "ECONNREFUSED on /tmp/x.sock" instead of the empty string the
|
|
24
|
+
* falkordb client surfaces by default.
|
|
25
|
+
*/
|
|
26
|
+
export declare function extractErrorInfo(err: unknown): Record<string, unknown>;
|
|
19
27
|
export declare class GraphStore {
|
|
20
28
|
private db;
|
|
21
29
|
private client;
|
|
@@ -23,6 +31,9 @@ export declare class GraphStore {
|
|
|
23
31
|
private available;
|
|
24
32
|
private persistenceDir;
|
|
25
33
|
private ownsServer;
|
|
34
|
+
private livenessProbeTimer;
|
|
35
|
+
private livenessFailureStreak;
|
|
36
|
+
private livenessRestartAttempts;
|
|
26
37
|
constructor(persistenceDir: string);
|
|
27
38
|
/** Get the socket file path for this instance's data dir. */
|
|
28
39
|
private get socketFilePath();
|
|
@@ -37,6 +48,19 @@ export declare class GraphStore {
|
|
|
37
48
|
*/
|
|
38
49
|
connectToRunning(): Promise<boolean>;
|
|
39
50
|
isAvailable(): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Periodic health check on the owned embedded server. Runs every 60s.
|
|
53
|
+
* Pings with a trivial Cypher query (RETURN 1). Two consecutive failures
|
|
54
|
+
* trigger a restart attempt. Backs off after 3 restart attempts to avoid
|
|
55
|
+
* crash-loop noise — at that point graph features stay disabled until
|
|
56
|
+
* the daemon is manually restarted.
|
|
57
|
+
*
|
|
58
|
+
* Daemon-only because client processes have their own reconnect loop on
|
|
59
|
+
* the .on('error') path. The probe specifically catches the case where
|
|
60
|
+
* the server hangs (no error event but stops responding).
|
|
61
|
+
*/
|
|
62
|
+
private startLivenessProbe;
|
|
63
|
+
private attemptServerRestart;
|
|
40
64
|
close(): Promise<void>;
|
|
41
65
|
upsertEntity(label: string, id: string, props: Record<string, any>): Promise<void>;
|
|
42
66
|
getEntity(label: string, id: string): Promise<EntityNode | null>;
|
|
@@ -22,6 +22,46 @@ import pino from 'pino';
|
|
|
22
22
|
const logger = pino({ name: 'clementine.graph' });
|
|
23
23
|
const GRAPH_NAME = 'clementine';
|
|
24
24
|
const WIKILINK_RE = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g;
|
|
25
|
+
/**
|
|
26
|
+
* Phase 13 — extract structured info from arbitrary error objects so log
|
|
27
|
+
* entries always carry SOMETHING useful even when err.message is empty.
|
|
28
|
+
* Node socket errors have .code, .errno, .syscall, .address, .port that
|
|
29
|
+
* tell us "ECONNREFUSED on /tmp/x.sock" instead of the empty string the
|
|
30
|
+
* falkordb client surfaces by default.
|
|
31
|
+
*/
|
|
32
|
+
export function extractErrorInfo(err) {
|
|
33
|
+
if (!err)
|
|
34
|
+
return { errKind: 'no-error-object' };
|
|
35
|
+
if (typeof err !== 'object')
|
|
36
|
+
return { errKind: 'primitive', err: String(err).slice(0, 200) };
|
|
37
|
+
const e = err;
|
|
38
|
+
const out = {};
|
|
39
|
+
if (typeof e.message === 'string' && e.message.length > 0)
|
|
40
|
+
out.errMessage = e.message.slice(0, 200);
|
|
41
|
+
if (typeof e.name === 'string')
|
|
42
|
+
out.errName = e.name;
|
|
43
|
+
if (e.code !== undefined)
|
|
44
|
+
out.errCode = e.code;
|
|
45
|
+
if (e.errno !== undefined)
|
|
46
|
+
out.errno = e.errno;
|
|
47
|
+
if (e.syscall !== undefined)
|
|
48
|
+
out.errSyscall = e.syscall;
|
|
49
|
+
if (e.address !== undefined)
|
|
50
|
+
out.errAddress = e.address;
|
|
51
|
+
if (e.port !== undefined)
|
|
52
|
+
out.errPort = e.port;
|
|
53
|
+
if (e.path !== undefined)
|
|
54
|
+
out.errPath = e.path;
|
|
55
|
+
// Fall back to the constructor name + JSON shape if nothing meaningful surfaced
|
|
56
|
+
if (Object.keys(out).length === 0) {
|
|
57
|
+
out.errKind = e.constructor?.name ?? 'unknown';
|
|
58
|
+
try {
|
|
59
|
+
out.errJson = JSON.stringify(e).slice(0, 300);
|
|
60
|
+
}
|
|
61
|
+
catch { /* ignore */ }
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
25
65
|
/** Well-known file where the daemon writes the socket path for other processes. */
|
|
26
66
|
const SOCKET_FILE_NAME = '.graph.sock';
|
|
27
67
|
export class GraphStore {
|
|
@@ -31,6 +71,9 @@ export class GraphStore {
|
|
|
31
71
|
available = false;
|
|
32
72
|
persistenceDir;
|
|
33
73
|
ownsServer = false;
|
|
74
|
+
livenessProbeTimer = null;
|
|
75
|
+
livenessFailureStreak = 0;
|
|
76
|
+
livenessRestartAttempts = 0;
|
|
34
77
|
constructor(persistenceDir) {
|
|
35
78
|
this.persistenceDir = persistenceDir;
|
|
36
79
|
}
|
|
@@ -53,15 +96,21 @@ export class GraphStore {
|
|
|
53
96
|
this.graph = this.db.selectGraph(GRAPH_NAME);
|
|
54
97
|
this.available = true;
|
|
55
98
|
this.ownsServer = true;
|
|
56
|
-
// Catch connection-level errors
|
|
99
|
+
// Catch connection-level errors with full diagnosis (Phase 13).
|
|
57
100
|
let serverErrorLogged = false;
|
|
58
101
|
this.db.on?.('error', (err) => {
|
|
59
102
|
if (!serverErrorLogged) {
|
|
60
103
|
serverErrorLogged = true;
|
|
61
|
-
logger.warn(
|
|
104
|
+
logger.warn(extractErrorInfo(err), 'FalkorDB server error — disabling graph features');
|
|
62
105
|
this.available = false;
|
|
63
106
|
}
|
|
64
107
|
});
|
|
108
|
+
// Phase 13 — server-side liveness probe with auto-restart.
|
|
109
|
+
// Periodically (60s) ping the embedded server with a tiny query.
|
|
110
|
+
// If the ping fails, the server has hung or quietly died. Auto-
|
|
111
|
+
// restart by reinitializing instead of leaving graph features
|
|
112
|
+
// silently broken until the next daemon restart.
|
|
113
|
+
this.startLivenessProbe();
|
|
65
114
|
// Write socket path so MCP/dashboard/assistant can connect
|
|
66
115
|
writeFileSync(this.socketFilePath, this.db.socketPath, 'utf-8');
|
|
67
116
|
// Create indexes for fast lookups
|
|
@@ -103,47 +152,62 @@ export class GraphStore {
|
|
|
103
152
|
this.graph = this.client.selectGraph(GRAPH_NAME);
|
|
104
153
|
this.available = true;
|
|
105
154
|
this.ownsServer = false;
|
|
106
|
-
// Catch connection-level errors: disable and start reconnect loop
|
|
155
|
+
// Catch connection-level errors: disable and start reconnect loop.
|
|
156
|
+
// Phase 13: capture FULL error context (errno, code, syscall) instead
|
|
157
|
+
// of just .message — falkordb client emits raw socket errors that
|
|
158
|
+
// often have empty .message strings, leaving us blind to root cause.
|
|
107
159
|
let errorHandled = false;
|
|
108
160
|
this.client.on?.('error', (err) => {
|
|
109
161
|
if (errorHandled)
|
|
110
162
|
return;
|
|
111
163
|
errorHandled = true;
|
|
112
|
-
|
|
164
|
+
const errInfo = extractErrorInfo(err);
|
|
165
|
+
logger.warn({ ...errInfo, pid: process.pid, socketPath }, 'FalkorDB connection lost — starting reconnect loop');
|
|
113
166
|
this.available = false;
|
|
114
167
|
try {
|
|
115
168
|
this.client?.disconnect?.();
|
|
116
169
|
}
|
|
117
170
|
catch { /* ignore */ }
|
|
118
|
-
// Reconnect
|
|
171
|
+
// Reconnect schedule (Phase 13): exponential backoff from 1s up to
|
|
172
|
+
// 60s for the first 6 attempts (covers transient blips fast), then
|
|
173
|
+
// 5min × 5 (handles a daemon-restart window), then a slow 30min
|
|
174
|
+
// probe forever. Total time to "give up fast retries" reduced from
|
|
175
|
+
// 150s (5×30s) to 109s (1+3+10+30+60+5*60) but the EARLY attempts
|
|
176
|
+
// are much more aggressive — most blips recover within seconds.
|
|
177
|
+
const SCHEDULE_MS = [
|
|
178
|
+
1_000, 3_000, 10_000, 30_000, 60_000,
|
|
179
|
+
5 * 60_000, 5 * 60_000, 5 * 60_000, 5 * 60_000, 5 * 60_000,
|
|
180
|
+
];
|
|
181
|
+
const SLOW_PROBE_MS = 30 * 60_000;
|
|
119
182
|
let attempts = 0;
|
|
120
183
|
const reconnectLoop = async () => {
|
|
121
184
|
attempts++;
|
|
122
185
|
try {
|
|
123
186
|
const reconnected = await this.connectToRunning();
|
|
124
187
|
if (reconnected) {
|
|
125
|
-
logger.info({ attempts }, 'FalkorDB reconnected');
|
|
126
|
-
return;
|
|
188
|
+
logger.info({ attempts, pid: process.pid }, 'FalkorDB reconnected');
|
|
189
|
+
return;
|
|
127
190
|
}
|
|
128
191
|
}
|
|
129
|
-
catch {
|
|
130
|
-
|
|
131
|
-
|
|
192
|
+
catch (retryErr) {
|
|
193
|
+
// Capture retry failures too — silent catch was hiding root cause
|
|
194
|
+
const ri = extractErrorInfo(retryErr);
|
|
195
|
+
logger.debug({ ...ri, attempts }, 'FalkorDB reconnect attempt failed');
|
|
132
196
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
else {
|
|
137
|
-
// Keep a slow background probe instead of giving up entirely
|
|
138
|
-
logger.warn({ attempts }, 'FalkorDB reconnect entering slow probe (every 30 min)');
|
|
139
|
-
setTimeout(reconnectLoop, 30 * 60_000);
|
|
197
|
+
const delay = SCHEDULE_MS[attempts - 1] ?? SLOW_PROBE_MS;
|
|
198
|
+
if (attempts === SCHEDULE_MS.length) {
|
|
199
|
+
logger.warn({ attempts, pid: process.pid }, 'FalkorDB reconnect exhausted fast-retry schedule — entering slow probe (every 30 min)');
|
|
140
200
|
}
|
|
201
|
+
setTimeout(reconnectLoop, delay);
|
|
141
202
|
};
|
|
142
|
-
setTimeout(reconnectLoop,
|
|
203
|
+
setTimeout(reconnectLoop, SCHEDULE_MS[0]);
|
|
143
204
|
});
|
|
144
205
|
return true;
|
|
145
206
|
}
|
|
146
|
-
catch {
|
|
207
|
+
catch (err) {
|
|
208
|
+
// Phase 13: log connect-time failures too (was swallowing them silently)
|
|
209
|
+
const errInfo = extractErrorInfo(err);
|
|
210
|
+
logger.debug({ ...errInfo, socketPath: this.socketFilePath }, 'FalkorDB initial connect failed');
|
|
147
211
|
this.available = false;
|
|
148
212
|
return false;
|
|
149
213
|
}
|
|
@@ -151,7 +215,101 @@ export class GraphStore {
|
|
|
151
215
|
isAvailable() {
|
|
152
216
|
return this.available;
|
|
153
217
|
}
|
|
218
|
+
// ── Liveness probe (daemon only — Phase 13) ──────────────────────────
|
|
219
|
+
/**
|
|
220
|
+
* Periodic health check on the owned embedded server. Runs every 60s.
|
|
221
|
+
* Pings with a trivial Cypher query (RETURN 1). Two consecutive failures
|
|
222
|
+
* trigger a restart attempt. Backs off after 3 restart attempts to avoid
|
|
223
|
+
* crash-loop noise — at that point graph features stay disabled until
|
|
224
|
+
* the daemon is manually restarted.
|
|
225
|
+
*
|
|
226
|
+
* Daemon-only because client processes have their own reconnect loop on
|
|
227
|
+
* the .on('error') path. The probe specifically catches the case where
|
|
228
|
+
* the server hangs (no error event but stops responding).
|
|
229
|
+
*/
|
|
230
|
+
startLivenessProbe() {
|
|
231
|
+
if (this.livenessProbeTimer)
|
|
232
|
+
return;
|
|
233
|
+
const PROBE_INTERVAL_MS = 60_000;
|
|
234
|
+
const MAX_RESTART_ATTEMPTS = 3;
|
|
235
|
+
const probe = async () => {
|
|
236
|
+
if (!this.ownsServer)
|
|
237
|
+
return; // safety — only the server owner probes
|
|
238
|
+
if (!this.available || !this.graph) {
|
|
239
|
+
// Already disabled — let the existing recovery paths run.
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
await Promise.race([
|
|
244
|
+
this.graph.query('RETURN 1 AS ping'),
|
|
245
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('probe-timeout')), 5_000)),
|
|
246
|
+
]);
|
|
247
|
+
// Probe succeeded — reset failure streak.
|
|
248
|
+
if (this.livenessFailureStreak > 0) {
|
|
249
|
+
logger.info({ priorFailures: this.livenessFailureStreak }, 'FalkorDB liveness probe recovered');
|
|
250
|
+
this.livenessFailureStreak = 0;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
this.livenessFailureStreak++;
|
|
255
|
+
logger.warn({ ...extractErrorInfo(err), streak: this.livenessFailureStreak }, 'FalkorDB liveness probe failed');
|
|
256
|
+
if (this.livenessFailureStreak >= 2) {
|
|
257
|
+
await this.attemptServerRestart(MAX_RESTART_ATTEMPTS);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
this.livenessProbeTimer = setInterval(probe, PROBE_INTERVAL_MS);
|
|
262
|
+
// Don't keep the daemon alive just for the probe.
|
|
263
|
+
this.livenessProbeTimer.unref?.();
|
|
264
|
+
}
|
|
265
|
+
async attemptServerRestart(maxAttempts) {
|
|
266
|
+
if (this.livenessRestartAttempts >= maxAttempts) {
|
|
267
|
+
logger.error({ attempts: this.livenessRestartAttempts }, 'FalkorDB restart attempts exhausted — graph features disabled until daemon restart');
|
|
268
|
+
this.available = false;
|
|
269
|
+
if (this.livenessProbeTimer) {
|
|
270
|
+
clearInterval(this.livenessProbeTimer);
|
|
271
|
+
this.livenessProbeTimer = null;
|
|
272
|
+
}
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
this.livenessRestartAttempts++;
|
|
276
|
+
logger.warn({ attempt: this.livenessRestartAttempts, max: maxAttempts }, 'FalkorDB restart attempt');
|
|
277
|
+
try {
|
|
278
|
+
// Tear down the current server gracefully.
|
|
279
|
+
try {
|
|
280
|
+
await this.db?.close?.();
|
|
281
|
+
}
|
|
282
|
+
catch { /* ignore */ }
|
|
283
|
+
this.db = null;
|
|
284
|
+
this.graph = null;
|
|
285
|
+
this.available = false;
|
|
286
|
+
try {
|
|
287
|
+
unlinkSync(this.socketFilePath);
|
|
288
|
+
}
|
|
289
|
+
catch { /* ignore */ }
|
|
290
|
+
// Re-initialize. initialize() will re-register error handlers and
|
|
291
|
+
// re-start the probe — but we don't want to start a NESTED probe,
|
|
292
|
+
// so clear the timer first.
|
|
293
|
+
if (this.livenessProbeTimer) {
|
|
294
|
+
clearInterval(this.livenessProbeTimer);
|
|
295
|
+
this.livenessProbeTimer = null;
|
|
296
|
+
}
|
|
297
|
+
this.livenessFailureStreak = 0;
|
|
298
|
+
await this.initialize();
|
|
299
|
+
if (this.available) {
|
|
300
|
+
logger.info({ attempt: this.livenessRestartAttempts }, 'FalkorDB restart succeeded');
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
logger.error(extractErrorInfo(err), 'FalkorDB restart attempt failed');
|
|
305
|
+
}
|
|
306
|
+
}
|
|
154
307
|
async close() {
|
|
308
|
+
// Stop the liveness probe before tearing down (Phase 13).
|
|
309
|
+
if (this.livenessProbeTimer) {
|
|
310
|
+
clearInterval(this.livenessProbeTimer);
|
|
311
|
+
this.livenessProbeTimer = null;
|
|
312
|
+
}
|
|
155
313
|
if (this.ownsServer && this.db) {
|
|
156
314
|
// Clean up socket file
|
|
157
315
|
try {
|
package/dist/tools/shared.d.ts
CHANGED
|
@@ -24,7 +24,6 @@ export declare const SOUL_FILE: string;
|
|
|
24
24
|
export declare const IDENTITY_FILE: string;
|
|
25
25
|
export declare const HEARTBEAT_FILE: string;
|
|
26
26
|
export declare const CRON_FILE: string;
|
|
27
|
-
export declare const PROFILES_DIR: string;
|
|
28
27
|
export declare const AGENTS_DIR: string;
|
|
29
28
|
export declare const TEAM_COMMS_LOG: string;
|
|
30
29
|
export declare const HANDOFFS_DIR: string;
|
package/dist/tools/shared.js
CHANGED
|
@@ -10,27 +10,12 @@ import path from 'node:path';
|
|
|
10
10
|
import pino from 'pino';
|
|
11
11
|
// ── Paths ──────────────────────────────────────────────────────────────
|
|
12
12
|
export const BASE_DIR = process.env.CLEMENTINE_HOME || path.join(os.homedir(), '.clementine');
|
|
13
|
+
import { parseEnvText } from '../config/env-parser.js';
|
|
13
14
|
function readEnvFile() {
|
|
14
15
|
const envPath = path.join(BASE_DIR, '.env');
|
|
15
16
|
if (!existsSync(envPath))
|
|
16
17
|
return {};
|
|
17
|
-
|
|
18
|
-
for (const line of readFileSync(envPath, 'utf-8').split('\n')) {
|
|
19
|
-
const trimmed = line.trim();
|
|
20
|
-
if (!trimmed || trimmed.startsWith('#'))
|
|
21
|
-
continue;
|
|
22
|
-
const eqIndex = trimmed.indexOf('=');
|
|
23
|
-
if (eqIndex === -1)
|
|
24
|
-
continue;
|
|
25
|
-
const key = trimmed.slice(0, eqIndex);
|
|
26
|
-
let value = trimmed.slice(eqIndex + 1);
|
|
27
|
-
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
28
|
-
(value.startsWith("'") && value.endsWith("'"))) {
|
|
29
|
-
value = value.slice(1, -1);
|
|
30
|
-
}
|
|
31
|
-
result[key] = value;
|
|
32
|
-
}
|
|
33
|
-
return result;
|
|
18
|
+
return parseEnvText(readFileSync(envPath, 'utf-8'));
|
|
34
19
|
}
|
|
35
20
|
export const env = readEnvFile();
|
|
36
21
|
export const VAULT_DIR = path.join(BASE_DIR, 'vault');
|
|
@@ -50,7 +35,6 @@ export const SOUL_FILE = path.join(SYSTEM_DIR, 'SOUL.md');
|
|
|
50
35
|
export const IDENTITY_FILE = path.join(SYSTEM_DIR, 'IDENTITY.md');
|
|
51
36
|
export const HEARTBEAT_FILE = path.join(SYSTEM_DIR, 'HEARTBEAT.md');
|
|
52
37
|
export const CRON_FILE = path.join(SYSTEM_DIR, 'CRON.md');
|
|
53
|
-
export const PROFILES_DIR = path.join(SYSTEM_DIR, 'profiles');
|
|
54
38
|
export const AGENTS_DIR = path.join(SYSTEM_DIR, 'agents');
|
|
55
39
|
export const TEAM_COMMS_LOG = path.join(BASE_DIR, 'logs', 'team-comms.jsonl');
|
|
56
40
|
export const HANDOFFS_DIR = path.join(BASE_DIR, 'handoffs');
|
package/dist/tools/team-tools.js
CHANGED
|
@@ -5,7 +5,7 @@ import { randomBytes } from 'node:crypto';
|
|
|
5
5
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { z } from 'zod';
|
|
8
|
-
import { ACTIVE_AGENT_SLUG, AGENTS_DIR, BASE_DIR, DELEGATIONS_BASE,
|
|
8
|
+
import { ACTIVE_AGENT_SLUG, AGENTS_DIR, BASE_DIR, DELEGATIONS_BASE, TEAM_COMMS_LOG, env, logger, parseTasks, textResult, } from './shared.js';
|
|
9
9
|
import { todayISO } from '../gateway/cron-scheduler.js';
|
|
10
10
|
async function loadTeamAgents() {
|
|
11
11
|
const matterMod = await import('gray-matter');
|
|
@@ -30,21 +30,9 @@ async function loadTeamAgents() {
|
|
|
30
30
|
}
|
|
31
31
|
catch { /* agents dir not readable */ }
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const slug = file.replace(/\.md$/, '');
|
|
37
|
-
if (seen.has(slug))
|
|
38
|
-
continue;
|
|
39
|
-
const { data } = (await import('gray-matter')).default(readFileSync(path.join(PROFILES_DIR, file), 'utf-8'));
|
|
40
|
-
const channelName = data.channelName ? String(data.channelName) : '';
|
|
41
|
-
if (!channelName)
|
|
42
|
-
continue;
|
|
43
|
-
agents.push({ slug, name: String(data.name ?? slug), channelName, canMessage: Array.isArray(data.canMessage) ? data.canMessage.map(String) : [], description: String(data.description ?? '') });
|
|
44
|
-
}
|
|
45
|
-
catch { /* skip */ }
|
|
46
|
-
}
|
|
47
|
-
}
|
|
33
|
+
// Phase 14: legacy PROFILES_DIR fallback removed — that directory has
|
|
34
|
+
// been empty in production for a long time and the new format
|
|
35
|
+
// (vault/00-System/agents/<slug>/agent.md) is the only source loaded above.
|
|
48
36
|
return agents;
|
|
49
37
|
}
|
|
50
38
|
function assertAgentCrudAllowed(action) {
|
|
@@ -11,28 +11,13 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
13
13
|
import path from 'node:path';
|
|
14
|
+
import { parseEnvText } from '../config/env-parser.js';
|
|
14
15
|
/** Re-parse .env independently of config.ts's module-level cache. */
|
|
15
16
|
function readEnv(baseDir) {
|
|
16
17
|
const envPath = path.join(baseDir, '.env');
|
|
17
18
|
if (!existsSync(envPath))
|
|
18
19
|
return {};
|
|
19
|
-
|
|
20
|
-
for (const line of readFileSync(envPath, 'utf-8').split('\n')) {
|
|
21
|
-
const trimmed = line.trim();
|
|
22
|
-
if (!trimmed || trimmed.startsWith('#'))
|
|
23
|
-
continue;
|
|
24
|
-
const eqIndex = trimmed.indexOf('=');
|
|
25
|
-
if (eqIndex === -1)
|
|
26
|
-
continue;
|
|
27
|
-
const key = trimmed.slice(0, eqIndex);
|
|
28
|
-
let value = trimmed.slice(eqIndex + 1);
|
|
29
|
-
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
30
|
-
(value.startsWith("'") && value.endsWith("'"))) {
|
|
31
|
-
value = value.slice(1, -1);
|
|
32
|
-
}
|
|
33
|
-
result[key] = value;
|
|
34
|
-
}
|
|
35
|
-
return result;
|
|
20
|
+
return parseEnvText(readFileSync(envPath, 'utf-8'));
|
|
36
21
|
}
|
|
37
22
|
function num(v) {
|
|
38
23
|
if (v === undefined || v === '')
|
package/package.json
CHANGED
package/dist/agent/profiles.d.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Clementine TypeScript — Agent profile management.
|
|
3
|
-
*
|
|
4
|
-
* Profiles are Markdown files with YAML frontmatter stored in
|
|
5
|
-
* vault/00-System/profiles/. Each profile defines a persona with its own
|
|
6
|
-
* tone, tool restrictions, security tier, and system prompt body.
|
|
7
|
-
*
|
|
8
|
-
* ProfileManager scans the directory, caches AgentProfile objects, and
|
|
9
|
-
* hot-reloads when files change (60-second TTL).
|
|
10
|
-
*/
|
|
11
|
-
import type { AgentProfile } from '../types.js';
|
|
12
|
-
export declare class ProfileManager {
|
|
13
|
-
private dir;
|
|
14
|
-
private cache;
|
|
15
|
-
private cacheTime;
|
|
16
|
-
constructor(profilesDir: string);
|
|
17
|
-
private refreshIfStale;
|
|
18
|
-
private loadProfile;
|
|
19
|
-
get(slug: string): AgentProfile | null;
|
|
20
|
-
listAll(): AgentProfile[];
|
|
21
|
-
}
|
|
22
|
-
//# sourceMappingURL=profiles.d.ts.map
|
package/dist/agent/profiles.js
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Clementine TypeScript — Agent profile management.
|
|
3
|
-
*
|
|
4
|
-
* Profiles are Markdown files with YAML frontmatter stored in
|
|
5
|
-
* vault/00-System/profiles/. Each profile defines a persona with its own
|
|
6
|
-
* tone, tool restrictions, security tier, and system prompt body.
|
|
7
|
-
*
|
|
8
|
-
* ProfileManager scans the directory, caches AgentProfile objects, and
|
|
9
|
-
* hot-reloads when files change (60-second TTL).
|
|
10
|
-
*/
|
|
11
|
-
import fs from 'node:fs';
|
|
12
|
-
import path from 'node:path';
|
|
13
|
-
import matter from 'gray-matter';
|
|
14
|
-
const CACHE_TTL_MS = 60_000;
|
|
15
|
-
export class ProfileManager {
|
|
16
|
-
dir;
|
|
17
|
-
cache = new Map();
|
|
18
|
-
cacheTime = 0;
|
|
19
|
-
constructor(profilesDir) {
|
|
20
|
-
this.dir = profilesDir;
|
|
21
|
-
}
|
|
22
|
-
refreshIfStale() {
|
|
23
|
-
const now = Date.now();
|
|
24
|
-
if (now - this.cacheTime < CACHE_TTL_MS && this.cache.size > 0) {
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
if (!fs.existsSync(this.dir)) {
|
|
28
|
-
this.cache.clear();
|
|
29
|
-
this.cacheTime = now;
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
const profiles = new Map();
|
|
33
|
-
const files = fs.readdirSync(this.dir).filter((f) => f.endsWith('.md') && !f.startsWith('_')).sort();
|
|
34
|
-
for (const file of files) {
|
|
35
|
-
try {
|
|
36
|
-
const filePath = path.join(this.dir, file);
|
|
37
|
-
const profile = this.loadProfile(filePath, file);
|
|
38
|
-
const slug = file.replace(/\.md$/, '');
|
|
39
|
-
profiles.set(slug, profile);
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
// Skip malformed profile files
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
this.cache = profiles;
|
|
46
|
-
this.cacheTime = now;
|
|
47
|
-
}
|
|
48
|
-
loadProfile(filePath, fileName) {
|
|
49
|
-
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
50
|
-
const { data: meta, content } = matter(raw);
|
|
51
|
-
const slug = fileName.replace(/\.md$/, '');
|
|
52
|
-
// Cap tier at 2 — profiles can never grant Tier 3
|
|
53
|
-
const tier = Math.min(Number(meta.tier ?? 1), 2);
|
|
54
|
-
// Parse team-specific frontmatter
|
|
55
|
-
let team;
|
|
56
|
-
const channelName = Array.isArray(meta.channelName)
|
|
57
|
-
? meta.channelName.map(String).filter(Boolean)
|
|
58
|
-
: meta.channelName ? String(meta.channelName) : undefined;
|
|
59
|
-
const canMessage = Array.isArray(meta.canMessage)
|
|
60
|
-
? meta.canMessage.map(String).filter(Boolean)
|
|
61
|
-
: [];
|
|
62
|
-
const allowedTools = Array.isArray(meta.allowedTools)
|
|
63
|
-
? meta.allowedTools.map(String).filter(Boolean)
|
|
64
|
-
: undefined;
|
|
65
|
-
if (channelName && (typeof channelName === 'string' || channelName.length > 0)) {
|
|
66
|
-
// channels[] populated at runtime by the agent's own bot
|
|
67
|
-
const teamChat = meta.teamChat === true || meta.teamChat === 'true';
|
|
68
|
-
const respondToAll = meta.respondToAll === true || meta.respondToAll === 'true';
|
|
69
|
-
team = { channelName, channels: [], canMessage, allowedTools, teamChat, respondToAll: respondToAll || undefined };
|
|
70
|
-
}
|
|
71
|
-
return {
|
|
72
|
-
slug,
|
|
73
|
-
name: String(meta.name ?? slug.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())),
|
|
74
|
-
tier,
|
|
75
|
-
description: String(meta.description ?? meta.role ?? ''),
|
|
76
|
-
systemPromptBody: content.trim(),
|
|
77
|
-
model: meta.model ? String(meta.model) : undefined,
|
|
78
|
-
avatar: meta.avatar ? String(meta.avatar) : undefined,
|
|
79
|
-
team,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
get(slug) {
|
|
83
|
-
this.refreshIfStale();
|
|
84
|
-
return this.cache.get(slug) ?? null;
|
|
85
|
-
}
|
|
86
|
-
listAll() {
|
|
87
|
-
this.refreshIfStale();
|
|
88
|
-
return [...this.cache.values()];
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
//# sourceMappingURL=profiles.js.map
|