clementine-agent 1.1.15 → 1.1.17
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 +28 -3
- package/dist/index.js +0 -1
- 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
|
}
|
|
@@ -732,6 +732,10 @@ export class Gateway {
|
|
|
732
732
|
});
|
|
733
733
|
}
|
|
734
734
|
async _handleMessageInner(sessionKey, text, onText, model, maxTurns, onToolActivity, onProgress) {
|
|
735
|
+
// Per-segment latency capture — emitted as a single 'chat:latency' line
|
|
736
|
+
// on the happy path so we can grep/aggregate without parsing many lines.
|
|
737
|
+
const tInnerStart = Date.now();
|
|
738
|
+
const timings = {};
|
|
735
739
|
// ── Auth circuit breaker — stop spamming error messages ────────
|
|
736
740
|
if (this.authCircuitOpen) {
|
|
737
741
|
if (!this.shouldProbeAuth()) {
|
|
@@ -765,6 +769,7 @@ export class Gateway {
|
|
|
765
769
|
await onProgress('thinking...').catch(() => { });
|
|
766
770
|
}
|
|
767
771
|
const laneWaitMs = Date.now() - laneWaitStart;
|
|
772
|
+
timings.laneWaitMs = laneWaitMs;
|
|
768
773
|
if (laneWaitMs > 1000) {
|
|
769
774
|
logger.info({ sessionKey, laneWaitMs }, 'Chat lane wait was non-trivial');
|
|
770
775
|
}
|
|
@@ -775,8 +780,10 @@ export class Gateway {
|
|
|
775
780
|
// ── Pre-flight injection scan ───────────────────────────────
|
|
776
781
|
// Re-baseline integrity before scanning — auto-memory, crons, and heartbeats
|
|
777
782
|
// legitimately modify vault files between messages. Skip if refreshed within 5s.
|
|
783
|
+
const tScanStart = Date.now();
|
|
778
784
|
scanner.refreshIfStale(5000);
|
|
779
785
|
const scan = scanner.scan(text);
|
|
786
|
+
timings.scanMs = Date.now() - tScanStart;
|
|
780
787
|
// Owner DMs are trusted — only block on high-confidence injection patterns,
|
|
781
788
|
// not integrity changes (which are usually caused by Clementine's own writes).
|
|
782
789
|
const isOwnerDm = sessionKey.startsWith('discord:user:') ||
|
|
@@ -873,9 +880,11 @@ export class Gateway {
|
|
|
873
880
|
if (!isInternalMsg && !sess?.profile && !text.startsWith('!') && !isStructuredWorkflowMsg && onProgress) {
|
|
874
881
|
await onProgress('checking if a teammate should handle this...').catch(() => { });
|
|
875
882
|
}
|
|
883
|
+
const tRoutingStart = Date.now();
|
|
876
884
|
const routingResult = !isInternalMsg && !sess?.profile && !text.startsWith('!') && !isStructuredWorkflowMsg
|
|
877
885
|
? await this._maybeRouteToSpecialist(sessionKey, text, onText)
|
|
878
886
|
: null;
|
|
887
|
+
timings.routingMs = Date.now() - tRoutingStart;
|
|
879
888
|
if (routingResult?.delegated) {
|
|
880
889
|
return routingResult.ackMessage;
|
|
881
890
|
}
|
|
@@ -1013,9 +1022,12 @@ export class Gateway {
|
|
|
1013
1022
|
let toolActivityCount = 0;
|
|
1014
1023
|
let lastStreamedText = '';
|
|
1015
1024
|
let lastProgressEmitAt = Date.now();
|
|
1025
|
+
let firstTokenAt;
|
|
1016
1026
|
const sessState = this.getSession(sessionKey);
|
|
1017
1027
|
const wrappedOnText = onText
|
|
1018
1028
|
? async (token) => {
|
|
1029
|
+
if (firstTokenAt === undefined)
|
|
1030
|
+
firstTokenAt = Date.now();
|
|
1019
1031
|
resetIdleTimer();
|
|
1020
1032
|
lastStreamedText = token;
|
|
1021
1033
|
// Mirror to session state so a concurrent acquireSessionLock()
|
|
@@ -1098,10 +1110,23 @@ export class Gateway {
|
|
|
1098
1110
|
if (cs)
|
|
1099
1111
|
delete cs.abortController;
|
|
1100
1112
|
}
|
|
1113
|
+
const chatMs = Date.now() - queryStartMs;
|
|
1114
|
+
timings.chatMs = chatMs;
|
|
1115
|
+
if (firstTokenAt !== undefined) {
|
|
1116
|
+
timings.firstTokenMs = firstTokenAt - queryStartMs;
|
|
1117
|
+
}
|
|
1101
1118
|
events.emit('query:complete', {
|
|
1102
1119
|
sessionKey, responseLength: response?.length ?? 0,
|
|
1103
|
-
toolActivityCount, durationMs:
|
|
1120
|
+
toolActivityCount, durationMs: chatMs,
|
|
1104
1121
|
});
|
|
1122
|
+
// One greppable line per chat completion — feed for the latency dashboard.
|
|
1123
|
+
logger.info({
|
|
1124
|
+
sessionKey,
|
|
1125
|
+
totalMs: Date.now() - tInnerStart,
|
|
1126
|
+
...timings,
|
|
1127
|
+
toolActivityCount,
|
|
1128
|
+
responseLen: response?.length ?? 0,
|
|
1129
|
+
}, 'chat:latency');
|
|
1105
1130
|
// Re-baseline integrity checksums after chat (auto-memory may write to vault)
|
|
1106
1131
|
scanner.refreshIntegrity();
|
|
1107
1132
|
// ── Auto-plan detection ──────────────────────────────────────
|
package/dist/index.js
CHANGED
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
|