clementine-agent 1.1.15 → 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.
@@ -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, legacyProfilesDir: 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
- import { ProfileManager } from './profiles.js';
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, legacyProfilesDir) {
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
  }
@@ -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, PROFILES_DIR, 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';
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, PROFILES_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 { PROFILES_DIR, AGENTS_DIR } = await import('../config.js');
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, PROFILES_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);
@@ -1633,8 +1633,7 @@ export async function cmdDashboard(opts) {
1633
1633
  }
1634
1634
  result.projects = { projects: cachedProjects ?? [] };
1635
1635
  try {
1636
- const profilesDir = path.join(VAULT_DIR, '00-System', 'profiles');
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 profilesDir = path.join(VAULT_DIR, '00-System', 'profiles');
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
- const profilesDir = path.join(VAULT_DIR, '00-System', 'profiles');
4282
- if (!existsSync(profilesDir)) {
4283
- res.json({ profiles: [], active: null });
4284
- return;
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 profilesDir = path.join(VAULT_DIR, '00-System', 'profiles');
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 profilesDir = path.join(VAULT_DIR, '00-System', 'profiles');
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
- const result = {};
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 function shellEscape(s: string): string;
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
- const result = {};
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
- export const PROFILES_DIR = path.join(SYSTEM_DIR, 'profiles');
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 function shellEscape(s) {
175
- return `'${s.replace(/'/g, "'\\''")}'`;
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).
@@ -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, PROFILES_DIR, AGENTS_DIR, TEAM_COMMS_LOG, BASE_DIR, SEEN_CHANNELS_FILE } from '../config.js';
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, PROFILES_DIR);
341
+ this._agentManager = new AgentManager(AGENTS_DIR);
342
342
  }
343
343
  return this._agentManager;
344
344
  }
package/dist/index.js CHANGED
@@ -328,7 +328,6 @@ function ensureVaultDirs() {
328
328
  config.TASKS_DIR,
329
329
  config.TEMPLATES_DIR,
330
330
  config.INBOX_DIR,
331
- config.PROFILES_DIR,
332
331
  ];
333
332
  for (const dir of dirs) {
334
333
  if (!existsSync(dir)) {
@@ -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;
@@ -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
- const result = {};
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');
@@ -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, PROFILES_DIR, TEAM_COMMS_LOG, env, logger, parseTasks, textResult, } from './shared.js';
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
- if (existsSync(PROFILES_DIR)) {
34
- for (const file of readdirSync(PROFILES_DIR).filter(f => f.endsWith('.md') && !f.startsWith('_'))) {
35
- try {
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
- const result = {};
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.1.15",
3
+ "version": "1.1.16",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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
@@ -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