clementine-agent 1.0.99 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/clementine-json.d.ts +57 -0
- package/dist/config/clementine-json.js +95 -0
- package/dist/config.js +19 -3
- package/dist/vault-migrations/0004-backfill-schema-versions.d.ts +13 -0
- package/dist/vault-migrations/0004-backfill-schema-versions.js +85 -0
- package/dist/vault-migrations/0005-create-clementine-json.d.ts +14 -0
- package/dist/vault-migrations/0005-create-clementine-json.js +133 -0
- package/package.json +1 -1
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine JSON config loader.
|
|
3
|
+
*
|
|
4
|
+
* `~/.clementine/clementine.json` is the canonical user-editable config file.
|
|
5
|
+
* Each field is optional — missing values fall through to .env, then to
|
|
6
|
+
* compiled defaults. Precedence (highest first):
|
|
7
|
+
*
|
|
8
|
+
* 1. process.env (CI/runtime overrides)
|
|
9
|
+
* 2. ~/.clementine/.env (existing user-edited config)
|
|
10
|
+
* 3. ~/.clementine/clementine.json (this file)
|
|
11
|
+
* 4. Compiled defaults
|
|
12
|
+
*
|
|
13
|
+
* The file is created on first run by the 0005 migration (kind: 'config').
|
|
14
|
+
* Loader validates with zod and falls back gracefully on malformed input —
|
|
15
|
+
* a corrupt file is logged and treated as empty.
|
|
16
|
+
*
|
|
17
|
+
* Cached by mtime so subsequent reads are O(1) absent file changes.
|
|
18
|
+
*/
|
|
19
|
+
import { z } from 'zod';
|
|
20
|
+
export declare const clementineJsonSchema: z.ZodObject<{
|
|
21
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
22
|
+
ownerName: z.ZodOptional<z.ZodString>;
|
|
23
|
+
assistantName: z.ZodOptional<z.ZodString>;
|
|
24
|
+
timezone: z.ZodOptional<z.ZodString>;
|
|
25
|
+
models: z.ZodOptional<z.ZodObject<{
|
|
26
|
+
default: z.ZodOptional<z.ZodString>;
|
|
27
|
+
haiku: z.ZodOptional<z.ZodString>;
|
|
28
|
+
sonnet: z.ZodOptional<z.ZodString>;
|
|
29
|
+
opus: z.ZodOptional<z.ZodString>;
|
|
30
|
+
}, z.core.$strip>>;
|
|
31
|
+
budgets: z.ZodOptional<z.ZodObject<{
|
|
32
|
+
heartbeat: z.ZodOptional<z.ZodNumber>;
|
|
33
|
+
cronT1: z.ZodOptional<z.ZodNumber>;
|
|
34
|
+
cronT2: z.ZodOptional<z.ZodNumber>;
|
|
35
|
+
chat: z.ZodOptional<z.ZodNumber>;
|
|
36
|
+
}, z.core.$strip>>;
|
|
37
|
+
heartbeat: z.ZodOptional<z.ZodObject<{
|
|
38
|
+
intervalMinutes: z.ZodOptional<z.ZodNumber>;
|
|
39
|
+
activeStart: z.ZodOptional<z.ZodNumber>;
|
|
40
|
+
activeEnd: z.ZodOptional<z.ZodNumber>;
|
|
41
|
+
}, z.core.$strip>>;
|
|
42
|
+
unleashed: z.ZodOptional<z.ZodObject<{
|
|
43
|
+
phaseTurns: z.ZodOptional<z.ZodNumber>;
|
|
44
|
+
defaultMaxHours: z.ZodOptional<z.ZodNumber>;
|
|
45
|
+
maxPhases: z.ZodOptional<z.ZodNumber>;
|
|
46
|
+
}, z.core.$strip>>;
|
|
47
|
+
}, z.core.$strip>;
|
|
48
|
+
export type ClementineJson = z.infer<typeof clementineJsonSchema>;
|
|
49
|
+
export declare function clementineJsonPath(baseDir: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Load and validate clementine.json. Returns an empty object if the file
|
|
52
|
+
* is missing, unreadable, or fails validation. Cached by mtime.
|
|
53
|
+
*/
|
|
54
|
+
export declare function loadClementineJson(baseDir: string): ClementineJson;
|
|
55
|
+
/** Test-only: clear the loader cache. */
|
|
56
|
+
export declare function _resetClementineJsonCache(): void;
|
|
57
|
+
//# sourceMappingURL=clementine-json.d.ts.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine JSON config loader.
|
|
3
|
+
*
|
|
4
|
+
* `~/.clementine/clementine.json` is the canonical user-editable config file.
|
|
5
|
+
* Each field is optional — missing values fall through to .env, then to
|
|
6
|
+
* compiled defaults. Precedence (highest first):
|
|
7
|
+
*
|
|
8
|
+
* 1. process.env (CI/runtime overrides)
|
|
9
|
+
* 2. ~/.clementine/.env (existing user-edited config)
|
|
10
|
+
* 3. ~/.clementine/clementine.json (this file)
|
|
11
|
+
* 4. Compiled defaults
|
|
12
|
+
*
|
|
13
|
+
* The file is created on first run by the 0005 migration (kind: 'config').
|
|
14
|
+
* Loader validates with zod and falls back gracefully on malformed input —
|
|
15
|
+
* a corrupt file is logged and treated as empty.
|
|
16
|
+
*
|
|
17
|
+
* Cached by mtime so subsequent reads are O(1) absent file changes.
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import pino from 'pino';
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
const logger = pino({ name: 'clementine.config-json' });
|
|
24
|
+
// ── Schema ───────────────────────────────────────────────────────────
|
|
25
|
+
export const clementineJsonSchema = z.object({
|
|
26
|
+
schemaVersion: z.literal(1),
|
|
27
|
+
ownerName: z.string().optional(),
|
|
28
|
+
assistantName: z.string().optional(),
|
|
29
|
+
timezone: z.string().optional(),
|
|
30
|
+
models: z.object({
|
|
31
|
+
default: z.string().optional(),
|
|
32
|
+
haiku: z.string().optional(),
|
|
33
|
+
sonnet: z.string().optional(),
|
|
34
|
+
opus: z.string().optional(),
|
|
35
|
+
}).optional(),
|
|
36
|
+
budgets: z.object({
|
|
37
|
+
heartbeat: z.number().nonnegative().optional(),
|
|
38
|
+
cronT1: z.number().nonnegative().optional(),
|
|
39
|
+
cronT2: z.number().nonnegative().optional(),
|
|
40
|
+
chat: z.number().nonnegative().optional(),
|
|
41
|
+
}).optional(),
|
|
42
|
+
heartbeat: z.object({
|
|
43
|
+
intervalMinutes: z.number().int().positive().optional(),
|
|
44
|
+
activeStart: z.number().int().min(0).max(23).optional(),
|
|
45
|
+
activeEnd: z.number().int().min(0).max(23).optional(),
|
|
46
|
+
}).optional(),
|
|
47
|
+
unleashed: z.object({
|
|
48
|
+
phaseTurns: z.number().int().positive().optional(),
|
|
49
|
+
defaultMaxHours: z.number().positive().optional(),
|
|
50
|
+
maxPhases: z.number().int().positive().optional(),
|
|
51
|
+
}).optional(),
|
|
52
|
+
});
|
|
53
|
+
const cache = new Map();
|
|
54
|
+
export function clementineJsonPath(baseDir) {
|
|
55
|
+
return path.join(baseDir, 'clementine.json');
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Load and validate clementine.json. Returns an empty object if the file
|
|
59
|
+
* is missing, unreadable, or fails validation. Cached by mtime.
|
|
60
|
+
*/
|
|
61
|
+
export function loadClementineJson(baseDir) {
|
|
62
|
+
const filePath = clementineJsonPath(baseDir);
|
|
63
|
+
if (!existsSync(filePath))
|
|
64
|
+
return { schemaVersion: 1 };
|
|
65
|
+
let mtime;
|
|
66
|
+
try {
|
|
67
|
+
mtime = statSync(filePath).mtimeMs;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return { schemaVersion: 1 };
|
|
71
|
+
}
|
|
72
|
+
const cached = cache.get(filePath);
|
|
73
|
+
if (cached && cached.mtime === mtime)
|
|
74
|
+
return cached.data;
|
|
75
|
+
let raw;
|
|
76
|
+
try {
|
|
77
|
+
raw = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
logger.warn({ err, filePath }, 'Failed to parse clementine.json — using empty config');
|
|
81
|
+
return { schemaVersion: 1 };
|
|
82
|
+
}
|
|
83
|
+
const parsed = clementineJsonSchema.safeParse(raw);
|
|
84
|
+
if (!parsed.success) {
|
|
85
|
+
logger.warn({ filePath, errors: parsed.error.issues.slice(0, 3) }, 'clementine.json failed validation — using empty config');
|
|
86
|
+
return { schemaVersion: 1 };
|
|
87
|
+
}
|
|
88
|
+
cache.set(filePath, { mtime, data: parsed.data });
|
|
89
|
+
return parsed.data;
|
|
90
|
+
}
|
|
91
|
+
/** Test-only: clear the loader cache. */
|
|
92
|
+
export function _resetClementineJsonCache() {
|
|
93
|
+
cache.clear();
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=clementine-json.js.map
|
package/dist/config.js
CHANGED
|
@@ -76,9 +76,25 @@ export const PROJECTS_META_FILE = path.join(BASE_DIR, 'projects.json');
|
|
|
76
76
|
export const WORKING_MEMORY_FILE = path.join(BASE_DIR, 'working-memory.md');
|
|
77
77
|
export const IDENTITY_FILE = path.join(SYSTEM_DIR, 'IDENTITY.md');
|
|
78
78
|
// ── Assistant identity ───────────────────────────────────────────────
|
|
79
|
-
|
|
79
|
+
// JSON config — loaded once at module init. Lower precedence than .env.
|
|
80
|
+
import { loadClementineJson } from './config/clementine-json.js';
|
|
81
|
+
const json = loadClementineJson(BASE_DIR);
|
|
82
|
+
/**
|
|
83
|
+
* Resolve a value with full precedence: process.env > .env > clementine.json > default.
|
|
84
|
+
* `getEnv` already covers the first two (process.env wins inside getEnv), so this
|
|
85
|
+
* helper just adds JSON as a fallback before the hardcoded default.
|
|
86
|
+
*/
|
|
87
|
+
function getEnvOrJson(envKey, jsonValue, fallback) {
|
|
88
|
+
const fromEnv = getEnv(envKey, '');
|
|
89
|
+
if (fromEnv)
|
|
90
|
+
return fromEnv;
|
|
91
|
+
if (jsonValue)
|
|
92
|
+
return jsonValue;
|
|
93
|
+
return fallback;
|
|
94
|
+
}
|
|
95
|
+
export const ASSISTANT_NAME = getEnvOrJson('ASSISTANT_NAME', json.assistantName, 'Clementine');
|
|
80
96
|
export const ASSISTANT_NICKNAME = getEnv('ASSISTANT_NICKNAME', 'Clemmy');
|
|
81
|
-
export const OWNER_NAME =
|
|
97
|
+
export const OWNER_NAME = getEnvOrJson('OWNER_NAME', json.ownerName, '');
|
|
82
98
|
// ── Secrets (with macOS Keychain fallback) ───────────────────────────
|
|
83
99
|
export function shellEscape(s) {
|
|
84
100
|
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
@@ -197,7 +213,7 @@ export const SF_API_VERSION = getEnv('SF_API_VERSION', 'v62.0');
|
|
|
197
213
|
export const ALLOW_ALL_USERS = getEnv('ALLOW_ALL_USERS', 'false').toLowerCase() === 'true';
|
|
198
214
|
// ── Timezone ─────────────────────────────────────────────────────────
|
|
199
215
|
/** User-configurable timezone. Falls back to system-detected timezone. */
|
|
200
|
-
export const TIMEZONE =
|
|
216
|
+
export const TIMEZONE = getEnvOrJson('TIMEZONE', json.timezone, Intl.DateTimeFormat().resolvedOptions().timeZone);
|
|
201
217
|
// ── Heartbeat ────────────────────────────────────────────────────────
|
|
202
218
|
export const HEARTBEAT_INTERVAL_MINUTES = parseInt(getEnv('HEARTBEAT_INTERVAL_MINUTES', '30'), 10) || 30;
|
|
203
219
|
export const HEARTBEAT_ACTIVE_START = parseInt(getEnv('HEARTBEAT_ACTIVE_START', '8'), 10);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration 0004: Backfill `schemaVersion: 1` into CRON.md and agent.md frontmatter.
|
|
3
|
+
*
|
|
4
|
+
* First multi-target migration — uses the new MigrationContext shape
|
|
5
|
+
* shipped in Phase 7a. Establishes the convention so future format changes
|
|
6
|
+
* (a v2 migration) can target files known to be at v1.
|
|
7
|
+
*
|
|
8
|
+
* Idempotent: parses with gray-matter, only writes when schemaVersion is
|
|
9
|
+
* missing or not equal to 1.
|
|
10
|
+
*/
|
|
11
|
+
import type { Migration } from './types.js';
|
|
12
|
+
export declare const migration: Migration;
|
|
13
|
+
//# sourceMappingURL=0004-backfill-schema-versions.d.ts.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration 0004: Backfill `schemaVersion: 1` into CRON.md and agent.md frontmatter.
|
|
3
|
+
*
|
|
4
|
+
* First multi-target migration — uses the new MigrationContext shape
|
|
5
|
+
* shipped in Phase 7a. Establishes the convention so future format changes
|
|
6
|
+
* (a v2 migration) can target files known to be at v1.
|
|
7
|
+
*
|
|
8
|
+
* Idempotent: parses with gray-matter, only writes when schemaVersion is
|
|
9
|
+
* missing or not equal to 1.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import matter from 'gray-matter';
|
|
14
|
+
function backfillFile(filePath) {
|
|
15
|
+
if (!existsSync(filePath))
|
|
16
|
+
return 'missing';
|
|
17
|
+
let parsed;
|
|
18
|
+
try {
|
|
19
|
+
parsed = matter(readFileSync(filePath, 'utf-8'));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return 'invalid';
|
|
23
|
+
}
|
|
24
|
+
const data = (parsed.data ?? {});
|
|
25
|
+
if (data.schemaVersion === 1)
|
|
26
|
+
return 'already-set';
|
|
27
|
+
data.schemaVersion = 1;
|
|
28
|
+
// gray-matter's stringify preserves the original frontmatter style.
|
|
29
|
+
writeFileSync(filePath, matter.stringify(parsed.content, data));
|
|
30
|
+
return 'updated';
|
|
31
|
+
}
|
|
32
|
+
function findAgentMdFiles(vaultDir) {
|
|
33
|
+
const out = [];
|
|
34
|
+
const agentsDir = path.join(vaultDir, '00-System', 'agents');
|
|
35
|
+
if (!existsSync(agentsDir))
|
|
36
|
+
return out;
|
|
37
|
+
for (const entry of readdirSync(agentsDir, { withFileTypes: true })) {
|
|
38
|
+
if (!entry.isDirectory())
|
|
39
|
+
continue;
|
|
40
|
+
const candidate = path.join(agentsDir, entry.name, 'agent.md');
|
|
41
|
+
if (existsSync(candidate))
|
|
42
|
+
out.push(candidate);
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
export const migration = {
|
|
47
|
+
kind: 'vault',
|
|
48
|
+
id: '0004-backfill-schema-versions',
|
|
49
|
+
description: 'Backfill schemaVersion: 1 into CRON.md and agent.md frontmatter',
|
|
50
|
+
apply(ctx) {
|
|
51
|
+
const targets = [];
|
|
52
|
+
const cronPath = path.join(ctx.vaultDir, '00-System', 'CRON.md');
|
|
53
|
+
if (existsSync(cronPath))
|
|
54
|
+
targets.push(cronPath);
|
|
55
|
+
targets.push(...findAgentMdFiles(ctx.vaultDir));
|
|
56
|
+
if (targets.length === 0) {
|
|
57
|
+
return { applied: false, skipped: true, details: 'No CRON.md or agent.md files found' };
|
|
58
|
+
}
|
|
59
|
+
const stats = { updated: 0, alreadySet: 0, invalid: 0, missing: 0 };
|
|
60
|
+
for (const f of targets) {
|
|
61
|
+
const r = backfillFile(f);
|
|
62
|
+
if (r === 'updated')
|
|
63
|
+
stats.updated++;
|
|
64
|
+
else if (r === 'already-set')
|
|
65
|
+
stats.alreadySet++;
|
|
66
|
+
else if (r === 'invalid')
|
|
67
|
+
stats.invalid++;
|
|
68
|
+
else
|
|
69
|
+
stats.missing++;
|
|
70
|
+
}
|
|
71
|
+
if (stats.updated === 0) {
|
|
72
|
+
return {
|
|
73
|
+
applied: false,
|
|
74
|
+
skipped: true,
|
|
75
|
+
details: `All ${targets.length} files already have schemaVersion (alreadySet=${stats.alreadySet}, invalid=${stats.invalid})`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
applied: true,
|
|
80
|
+
skipped: false,
|
|
81
|
+
details: `Updated ${stats.updated}/${targets.length} files (alreadySet=${stats.alreadySet}, invalid=${stats.invalid})`,
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
//# sourceMappingURL=0004-backfill-schema-versions.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration 0005: Create ~/.clementine/clementine.json with sane defaults.
|
|
3
|
+
*
|
|
4
|
+
* First config-kind migration — uses MigrationContext.baseDir, not vaultDir.
|
|
5
|
+
* Reads the existing .env (already loaded into process at module init via
|
|
6
|
+
* config.ts, so we re-read here to be safe) and writes the equivalent
|
|
7
|
+
* canonical config file with comments explaining precedence rules.
|
|
8
|
+
*
|
|
9
|
+
* Idempotent: skips entirely if the file already exists. Users who edit
|
|
10
|
+
* the file post-migration are not at risk of having their edits overwritten.
|
|
11
|
+
*/
|
|
12
|
+
import type { Migration } from './types.js';
|
|
13
|
+
export declare const migration: Migration;
|
|
14
|
+
//# sourceMappingURL=0005-create-clementine-json.d.ts.map
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration 0005: Create ~/.clementine/clementine.json with sane defaults.
|
|
3
|
+
*
|
|
4
|
+
* First config-kind migration — uses MigrationContext.baseDir, not vaultDir.
|
|
5
|
+
* Reads the existing .env (already loaded into process at module init via
|
|
6
|
+
* config.ts, so we re-read here to be safe) and writes the equivalent
|
|
7
|
+
* canonical config file with comments explaining precedence rules.
|
|
8
|
+
*
|
|
9
|
+
* Idempotent: skips entirely if the file already exists. Users who edit
|
|
10
|
+
* the file post-migration are not at risk of having their edits overwritten.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
/** Re-parse .env independently of config.ts's module-level cache. */
|
|
15
|
+
function readEnv(baseDir) {
|
|
16
|
+
const envPath = path.join(baseDir, '.env');
|
|
17
|
+
if (!existsSync(envPath))
|
|
18
|
+
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;
|
|
36
|
+
}
|
|
37
|
+
function num(v) {
|
|
38
|
+
if (v === undefined || v === '')
|
|
39
|
+
return undefined;
|
|
40
|
+
const n = Number(v);
|
|
41
|
+
return Number.isFinite(n) ? n : undefined;
|
|
42
|
+
}
|
|
43
|
+
export const migration = {
|
|
44
|
+
kind: 'config',
|
|
45
|
+
id: '0005-create-clementine-json',
|
|
46
|
+
description: 'Create clementine.json with current effective config as defaults',
|
|
47
|
+
apply(ctx) {
|
|
48
|
+
const targetPath = path.join(ctx.baseDir, 'clementine.json');
|
|
49
|
+
if (existsSync(targetPath)) {
|
|
50
|
+
return { applied: false, skipped: true, details: 'clementine.json already exists' };
|
|
51
|
+
}
|
|
52
|
+
const env = readEnv(ctx.baseDir);
|
|
53
|
+
// Build the canonical JSON. Only include fields we actually have values
|
|
54
|
+
// for — empty/missing fields are omitted so the file documents what was
|
|
55
|
+
// explicitly configured (not a wall of nulls).
|
|
56
|
+
const out = { schemaVersion: 1 };
|
|
57
|
+
if (env['OWNER_NAME'])
|
|
58
|
+
out.ownerName = env['OWNER_NAME'];
|
|
59
|
+
if (env['ASSISTANT_NAME'])
|
|
60
|
+
out.assistantName = env['ASSISTANT_NAME'];
|
|
61
|
+
if (env['TIMEZONE'])
|
|
62
|
+
out.timezone = env['TIMEZONE'];
|
|
63
|
+
const models = {};
|
|
64
|
+
if (env['DEFAULT_MODEL_TIER'])
|
|
65
|
+
models.default = env['DEFAULT_MODEL_TIER'];
|
|
66
|
+
if (env['HAIKU_MODEL'])
|
|
67
|
+
models.haiku = env['HAIKU_MODEL'];
|
|
68
|
+
if (env['SONNET_MODEL'])
|
|
69
|
+
models.sonnet = env['SONNET_MODEL'];
|
|
70
|
+
if (env['OPUS_MODEL'])
|
|
71
|
+
models.opus = env['OPUS_MODEL'];
|
|
72
|
+
if (Object.keys(models).length > 0)
|
|
73
|
+
out.models = models;
|
|
74
|
+
const budgets = {};
|
|
75
|
+
const bH = num(env['BUDGET_HEARTBEAT_USD']);
|
|
76
|
+
const bT1 = num(env['BUDGET_CRON_T1_USD']);
|
|
77
|
+
const bT2 = num(env['BUDGET_CRON_T2_USD']);
|
|
78
|
+
const bC = num(env['BUDGET_CHAT_USD']);
|
|
79
|
+
if (bH !== undefined)
|
|
80
|
+
budgets.heartbeat = bH;
|
|
81
|
+
if (bT1 !== undefined)
|
|
82
|
+
budgets.cronT1 = bT1;
|
|
83
|
+
if (bT2 !== undefined)
|
|
84
|
+
budgets.cronT2 = bT2;
|
|
85
|
+
if (bC !== undefined)
|
|
86
|
+
budgets.chat = bC;
|
|
87
|
+
if (Object.keys(budgets).length > 0)
|
|
88
|
+
out.budgets = budgets;
|
|
89
|
+
// Write as JSON with a leading comment-as-docstring isn't valid JSON.
|
|
90
|
+
// Instead we ship a sibling README that explains, and keep the JSON pure.
|
|
91
|
+
const tmp = `${targetPath}.${process.pid}.${Date.now()}.tmp`;
|
|
92
|
+
writeFileSync(tmp, JSON.stringify(out, null, 2) + '\n');
|
|
93
|
+
renameSync(tmp, targetPath);
|
|
94
|
+
// Also ship a README.md sibling explaining what this file is.
|
|
95
|
+
const readmePath = path.join(ctx.baseDir, 'README.md');
|
|
96
|
+
if (!existsSync(readmePath)) {
|
|
97
|
+
writeFileSync(readmePath, [
|
|
98
|
+
'# ~/.clementine/',
|
|
99
|
+
'',
|
|
100
|
+
'This is your Clementine data home. Updates to the engine (`npm update -g clementine-agent`)',
|
|
101
|
+
'replace the engine code but never touch this directory.',
|
|
102
|
+
'',
|
|
103
|
+
'## Editable config files',
|
|
104
|
+
'',
|
|
105
|
+
'- `clementine.json` — canonical user config (created by migration 0005)',
|
|
106
|
+
'- `.env` — secrets and per-machine overrides; takes precedence over `clementine.json`',
|
|
107
|
+
'- `vault/00-System/CRON.md` — cron jobs (YAML frontmatter)',
|
|
108
|
+
'- `vault/00-System/SOUL.md` — assistant personality / values',
|
|
109
|
+
'- `vault/00-System/agents/<slug>/agent.md` — per-agent definitions',
|
|
110
|
+
'- `advisor-rules/user/*.yaml` — custom advisor rules (overrides shipped builtins)',
|
|
111
|
+
'- `prompt-overrides/{_global.md,jobs/<name>.md,agents/<slug>.md}` — prompt augmentation',
|
|
112
|
+
'',
|
|
113
|
+
'## Config precedence (highest first)',
|
|
114
|
+
'',
|
|
115
|
+
'1. `process.env` — runtime overrides',
|
|
116
|
+
'2. `.env` in this dir — explicit per-machine config',
|
|
117
|
+
'3. `clementine.json` — canonical config',
|
|
118
|
+
'4. Compiled defaults',
|
|
119
|
+
'',
|
|
120
|
+
'## State and cache',
|
|
121
|
+
'',
|
|
122
|
+
'Other files in this directory are runtime state (sessions, heartbeat, logs)',
|
|
123
|
+
'or caches (memory.db, tool inventory). They are not for editing by hand.',
|
|
124
|
+
].join('\n') + '\n');
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
applied: true,
|
|
128
|
+
skipped: false,
|
|
129
|
+
details: `Created clementine.json with ${Object.keys(out).length - 1} field(s) populated`,
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
//# sourceMappingURL=0005-create-clementine-json.js.map
|