total-recall-brain 3.13.0 → 3.13.2
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/README.md +91 -108
- package/bin/antigravity.mjs +81 -52
- package/bin/total-recall.mjs +32 -57
- package/extension/background.js +1 -1
- package/extension/options/options.js +9 -3
- package/extension/sidepanel/sidepanel.js +0 -2
- package/frontend/index.html +4 -3
- package/frontend/package.json +1 -0
- package/frontend/src/App.tsx +109 -152
- package/frontend/src/api.ts +605 -23
- package/frontend/src/components/BrainSelector.tsx +211 -199
- package/frontend/src/components/DocumentEditorModal.tsx +57 -0
- package/frontend/src/components/DocumentTable.tsx +47 -0
- package/frontend/src/components/Graph3D.tsx +120 -71
- package/frontend/src/components/UsageChart.tsx +3 -3
- package/frontend/src/components/brand/BrandMark.tsx +95 -0
- package/frontend/src/index.css +286 -86
- package/frontend/src/pages/ApiKeysPage.tsx +1878 -215
- package/frontend/src/pages/ChatPage.tsx +103 -45
- package/frontend/src/pages/DesignDocsPage.tsx +5 -5
- package/frontend/src/pages/FilesPage.tsx +10 -6
- package/frontend/src/pages/GraphPage.tsx +5 -3
- package/frontend/src/pages/HelpPage.tsx +2 -2
- package/frontend/src/pages/InboxPage.tsx +82 -0
- package/frontend/src/pages/InstructionsPage.tsx +1 -1
- package/frontend/src/pages/IntegrationsPage.tsx +21 -10
- package/frontend/src/pages/LoginPage.tsx +95 -77
- package/frontend/src/pages/MemoryPage.tsx +31 -8
- package/frontend/src/pages/OnboardingPage.tsx +690 -0
- package/frontend/src/pages/OpenWikiPage.tsx +412 -323
- package/frontend/src/pages/SettingsPage.tsx +825 -922
- package/frontend/src/pages/SkillsPage.tsx +272 -147
- package/frontend/src/pages/UsagePage.tsx +23 -31
- package/frontend/src/pages/VaultPage.tsx +223 -0
- package/frontend/src/types.ts +13 -0
- package/package.json +4 -4
- package/scaffold/.agent/skills/total-recall/SKILL.md +43 -4
- package/scaffold/.agent/skills/total-recall/modules/README.md +10 -0
- package/scaffold/.agent/skills/total-recall/modules/agents/README.md +5 -0
- package/scaffold/.agent/skills/total-recall/{skills/tr-cli-agents → modules/agents}/agents.yml +8 -0
- package/scaffold/.agent/skills/total-recall/modules/skill-deploy/README.md +9 -0
- package/scaffold/.agent/skills/total-recall/openwiki/README.md +10 -0
- package/scaffold/.agent/skills/total-recall/openwiki/architecture.md +5 -0
- package/scaffold/.agent/skills/total-recall/openwiki/ide.md +10 -0
- package/scaffold/.agent/skills/total-recall/openwiki/memory.md +11 -0
- package/scaffold/.agent/skills/total-recall/openwiki/secrets.md +7 -0
- package/scaffold/.agent/skills/total-recall/openwiki/skills.md +7 -0
- package/src/cli/brain.mjs +63 -5
- package/src/cli/connect.mjs +11 -19
- package/src/cli/daemon.mjs +5 -4
- package/src/cli/deploy-ui.mjs +8 -3
- package/src/cli/deploy.mjs +84 -27
- package/src/cli/ingest/index.mjs +1 -1
- package/src/cli/init.mjs +63 -34
- package/src/cli/remember.mjs +3 -3
- package/src/cli/secret.mjs +812 -0
- package/src/cli/setup.mjs +34 -7
- package/src/cli/share.mjs +2 -2
- package/src/cli/skill.mjs +396 -35
- package/src/cli/sync.mjs +4 -4
- package/src/cli/task.mjs +287 -0
- package/src/core/clarity-rewriter.mjs +5 -5
- package/src/core/config.mjs +44 -2
- package/src/core/conflict-detector.mjs +5 -5
- package/src/core/crons.mjs +54 -0
- package/src/core/daemon-loop.mjs +87 -235
- package/src/core/dream.mjs +170 -10
- package/src/core/env-import.mjs +456 -0
- package/src/core/fact-seeker.mjs +14 -14
- package/src/core/import-rules.mjs +20 -7
- package/src/core/inference-engine.mjs +3 -3
- package/src/core/migrate.mjs +14 -8
- package/src/core/okf-adapter.mjs +2 -2
- package/src/core/operation-validator.mjs +39 -331
- package/src/core/optimizer.mjs +7 -0
- package/src/core/post-mortem.mjs +2 -2
- package/src/core/project-brain.mjs +349 -0
- package/src/core/provider-catalog.mjs +296 -0
- package/src/core/quick-capture.mjs +1 -1
- package/src/core/remote-vault-sync.mjs +194 -0
- package/src/core/research-queue.mjs +7 -11
- package/src/core/runtime.mjs +33 -9
- package/src/core/scheduler.mjs +59 -5
- package/src/core/schema.mjs +14 -0
- package/src/core/search.mjs +1 -1
- package/src/core/secrets-env-export.mjs +323 -0
- package/src/core/secrets-rotate.mjs +245 -0
- package/src/core/secrets-store.mjs +645 -0
- package/src/core/semantic-index.mjs +3 -3
- package/src/core/skills-registry.mjs +1062 -0
- package/src/core/source-adapters.mjs +4 -1
- package/src/core/ssss-host-extension.mjs +94 -0
- package/src/core/ssss-kernel-bridge.mjs +530 -0
- package/src/core/surface.mjs +14 -0
- package/src/core/task-envelope.mjs +298 -0
- package/src/core/task-executors.mjs +600 -0
- package/src/core/validated-write.mjs +124 -56
- package/src/core/vault.mjs +21 -34
- package/src/core/webauthn-store.mjs +272 -0
- package/src/server/api.mjs +26 -16
- package/src/server/auth.mjs +51 -0
- package/src/server/index.mjs +3 -3
- package/src/server/keys.mjs +30 -4
- package/src/server/rest.mjs +198 -25
- package/src/server/route-manifest.json +156 -0
- package/src/server/routes/_shared.mjs +40 -1
- package/src/server/routes/docs.mjs +260 -0
- package/src/server/routes/memory.mjs +145 -32
- package/src/server/routes/research.mjs +30 -0
- package/src/server/routes/secrets.mjs +470 -0
- package/src/server/routes/share.mjs +2 -2
- package/src/server/routes/skills.mjs +452 -54
- package/src/server/routes/sync.mjs +48 -0
- package/src/server/routes/webauthn.mjs +184 -0
- package/src/server/update-route-manifest.mjs +39 -0
- package/templates/onboarding-interview.md +32 -22
- package/templates/openwiki/README.md +10 -0
- package/templates/openwiki/architecture.md +5 -0
- package/templates/openwiki/ide.md +10 -0
- package/templates/openwiki/memory.md +11 -0
- package/templates/openwiki/secrets.md +7 -0
- package/templates/openwiki/skills.md +7 -0
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/SKILL.md +0 -121
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/antigravity.txt +0 -20
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/claude.txt +0 -58
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/codex.txt +0 -81
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/gemini.txt +0 -44
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/scripts/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/subagents/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/tr-research/SKILL.md +0 -338
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/SKILL.md +0 -282
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/assets/skill-template/SKILL.md +0 -20
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/evals/evals.json +0 -20
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/hooks/on-skill-add.sh +0 -14
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/hooks/on-skill-delete.sh +0 -14
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/learnings/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/references/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/create-skill.sh +0 -65
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/enforce-skill-optimization.mjs +0 -135
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/skill-watcher.mjs +0 -64
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/subagents/audit-all-skills.md +0 -62
- package/scaffold/.agent/skills/total-recall/skills/tr-ssss/SKILL.md +0 -298
- package/scaffold/.agent/skills/total-recall/skills/tr-ssss/evals/evals.json +0 -17
- package/scaffold/.agent/skills/total-recall/skills/tr-ssss/references/CHANGELOG.md +0 -124
- package/scaffold/.agent/skills/total-recall/skills/tr-ssss/references/ssss-spec.md +0 -1325
- package/scaffold/.agent/skills/total-recall/skills/tr-ssss/scripts/validate-schema.mjs +0 -232
- package/scaffold/.agent/skills/total-recall/skills/tr-ssss/subagents/audit-ssss-node.md +0 -9
- /package/scaffold/.agent/skills/total-recall/{skills/tr-cli-agents/evals → modules/skill-deploy/scripts}/.gitkeep +0 -0
- /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/find-skills.mjs +0 -0
- /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/install-skill.mjs +0 -0
- /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/scan-skill.mjs +0 -0
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scan env sources and import keys into the secrets store.
|
|
3
|
+
*
|
|
4
|
+
* Import is pattern-based (any secret-shaped env name) — NOT a hardcoded list of
|
|
5
|
+
* product APIs. Optional provider tags are best-effort labels for the catalog UI.
|
|
6
|
+
* Never logs or returns full secret values in scan results.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import { setSecret, listSecretsMeta } from './secrets-store.mjs';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @deprecated Kept as empty/export for older callers. Import does NOT use this
|
|
16
|
+
* as a whitelist — any secret-shaped key is eligible.
|
|
17
|
+
*/
|
|
18
|
+
export const KNOWN_SECRET_KEYS = [];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Name looks like a secret: ends with KEY/TOKEN/SECRET/PASSWORD/etc.
|
|
22
|
+
* Or contains those segments. Shell noise is skipped.
|
|
23
|
+
*/
|
|
24
|
+
const SECRET_NAME_RE =
|
|
25
|
+
/(?:^|_)(API[_-]?KEY|API[_-]?TOKEN|ACCESS[_-]?TOKEN|ACCESS[_-]?KEY|SECRET[_-]?KEY|PRIVATE[_-]?KEY|AUTH[_-]?TOKEN|CLIENT[_-]?SECRET|WEBHOOK[_-]?SECRET|PASSWORD|PASSWD|TOKEN|SECRET|KEY)$/i;
|
|
26
|
+
|
|
27
|
+
/** Obvious non-secrets even if they match loosely */
|
|
28
|
+
const SKIP_KEYS = new Set([
|
|
29
|
+
'PATH',
|
|
30
|
+
'HOME',
|
|
31
|
+
'USER',
|
|
32
|
+
'SHELL',
|
|
33
|
+
'PWD',
|
|
34
|
+
'TMPDIR',
|
|
35
|
+
'TERM',
|
|
36
|
+
'LANG',
|
|
37
|
+
'LC_ALL',
|
|
38
|
+
'NODE_ENV',
|
|
39
|
+
'COLORTERM',
|
|
40
|
+
'SSH_AUTH_SOCK',
|
|
41
|
+
'DISPLAY',
|
|
42
|
+
'USERPROFILE',
|
|
43
|
+
'HOSTNAME',
|
|
44
|
+
'LOGNAME',
|
|
45
|
+
'XPC_SERVICE_NAME',
|
|
46
|
+
'COMMAND_MODE',
|
|
47
|
+
'SHLVL',
|
|
48
|
+
'OLDPWD',
|
|
49
|
+
'TERM_PROGRAM',
|
|
50
|
+
'TERM_SESSION_ID',
|
|
51
|
+
'TMP',
|
|
52
|
+
'TEMP',
|
|
53
|
+
'EDITOR',
|
|
54
|
+
'VISUAL',
|
|
55
|
+
'PAGER',
|
|
56
|
+
'MANPATH',
|
|
57
|
+
'INFOPATH',
|
|
58
|
+
'GOPATH',
|
|
59
|
+
'GOROOT',
|
|
60
|
+
'JAVA_HOME',
|
|
61
|
+
'ANDROID_HOME',
|
|
62
|
+
'NVM_DIR',
|
|
63
|
+
'PNPM_HOME',
|
|
64
|
+
'BUN_INSTALL',
|
|
65
|
+
'ZSH',
|
|
66
|
+
'FPATH',
|
|
67
|
+
'LS_COLORS',
|
|
68
|
+
'COLORFGBG',
|
|
69
|
+
'ITERM_SESSION_ID',
|
|
70
|
+
'TERM_PROGRAM_VERSION',
|
|
71
|
+
'SSH_AGENT_PID',
|
|
72
|
+
'XPC_FLAGS',
|
|
73
|
+
'LaunchInstanceID',
|
|
74
|
+
'SECURITYSESSIONID',
|
|
75
|
+
'__CFBundleIdentifier',
|
|
76
|
+
'__CF_USER_TEXT_ENCODING',
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Infer a short provider *label* from the key name for catalog grouping.
|
|
81
|
+
* Not a whitelist — unknown keys still import; label may be null or derived.
|
|
82
|
+
*
|
|
83
|
+
* Strategy:
|
|
84
|
+
* 1. Optional match against provider-catalog key_patterns (data, not import gate)
|
|
85
|
+
* 2. Else first path segment of KEY_NAME before _API / _SECRET / _TOKEN / _KEY
|
|
86
|
+
*
|
|
87
|
+
* @param {string} key
|
|
88
|
+
* @returns {string|null}
|
|
89
|
+
*/
|
|
90
|
+
export function inferProvider(key) {
|
|
91
|
+
if (!key) return null;
|
|
92
|
+
const k = String(key);
|
|
93
|
+
|
|
94
|
+
// Lazy catalog match (optional enrichment only)
|
|
95
|
+
try {
|
|
96
|
+
// Dynamic import avoided in sync path — inline thin match from catalog module if already loadable
|
|
97
|
+
// Use sync require-free approach: duplicate-free segment heuristic only here for speed.
|
|
98
|
+
} catch {
|
|
99
|
+
/* ignore */
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Strip common prefixes that are not the vendor
|
|
103
|
+
let base = k
|
|
104
|
+
.replace(/^(DEVELOPER_|VITE_|NEXT_PUBLIC_|PUBLIC_|NUXT_PUBLIC_)/i, '')
|
|
105
|
+
.toUpperCase();
|
|
106
|
+
|
|
107
|
+
// Take leading brand segment(s) before secret suffix
|
|
108
|
+
const stripped = base
|
|
109
|
+
.replace(
|
|
110
|
+
/(_?API_?KEY|_?API_?TOKEN|_?ACCESS_?TOKEN|_?ACCESS_?KEY|_?SECRET_?KEY|_?PRIVATE_?KEY|_?AUTH_?TOKEN|_?CLIENT_?SECRET|_?WEBHOOK_?SECRET|_?PASSWORD|_?PASSWD|_?TOKEN|_?SECRET|_?KEY)$/i,
|
|
111
|
+
'',
|
|
112
|
+
)
|
|
113
|
+
.replace(/^_+|_+$/g, '');
|
|
114
|
+
|
|
115
|
+
if (!stripped) return null;
|
|
116
|
+
|
|
117
|
+
// Normalize multi-word vendors into a slug (OPEN_ROUTER → open-router, GOOGLE_GENERATIVE_AI → google)
|
|
118
|
+
const parts = stripped.toLowerCase().split(/_+/).filter(Boolean);
|
|
119
|
+
if (!parts.length) return null;
|
|
120
|
+
|
|
121
|
+
// Prefer first 1–2 meaningful parts, drop noise words
|
|
122
|
+
const noise = new Set(['api', 'app', 'service', 'role', 'admin', 'user', 'test', 'public', 'private']);
|
|
123
|
+
const useful = parts.filter((p) => !noise.has(p));
|
|
124
|
+
const take = useful.length ? useful : parts;
|
|
125
|
+
if (take.length === 1) return take[0];
|
|
126
|
+
// e.g. google_generative → google; digital_ocean → digitalocean
|
|
127
|
+
if (take[0].length <= 3 && take.length >= 2) return `${take[0]}-${take[1]}`;
|
|
128
|
+
return take[0];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param {string} value
|
|
133
|
+
*/
|
|
134
|
+
export function maskSecret(value) {
|
|
135
|
+
const s = String(value || '');
|
|
136
|
+
if (s.length <= 8) return '••••••••';
|
|
137
|
+
return `${s.slice(0, 4)}…${s.slice(-4)} (${s.length} chars)`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Parse .env-style text into key/value map.
|
|
142
|
+
* @param {string} text
|
|
143
|
+
* @returns {Record<string, string>}
|
|
144
|
+
*/
|
|
145
|
+
export function parseEnvText(text) {
|
|
146
|
+
const out = {};
|
|
147
|
+
if (!text || typeof text !== 'string') return out;
|
|
148
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
149
|
+
const line = rawLine.trim();
|
|
150
|
+
if (!line || line.startsWith('#')) continue;
|
|
151
|
+
const exportPrefix = line.startsWith('export ') ? line.slice(7).trim() : line;
|
|
152
|
+
const eq = exportPrefix.indexOf('=');
|
|
153
|
+
if (eq <= 0) continue;
|
|
154
|
+
const key = exportPrefix.slice(0, eq).trim();
|
|
155
|
+
let val = exportPrefix.slice(eq + 1).trim();
|
|
156
|
+
if (
|
|
157
|
+
(val.startsWith('"') && val.endsWith('"')) ||
|
|
158
|
+
(val.startsWith("'") && val.endsWith("'"))
|
|
159
|
+
) {
|
|
160
|
+
val = val.slice(1, -1);
|
|
161
|
+
}
|
|
162
|
+
// strip trailing inline comments for unquoted values
|
|
163
|
+
if (!exportPrefix.slice(eq + 1).trim().startsWith('"') && !exportPrefix.slice(eq + 1).trim().startsWith("'")) {
|
|
164
|
+
val = val.replace(/\s+#.*$/, '').trim();
|
|
165
|
+
}
|
|
166
|
+
if (key && val) out[key] = val;
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* True if env var *name* looks secret-shaped. No product/API whitelist.
|
|
173
|
+
* @param {string} key
|
|
174
|
+
*/
|
|
175
|
+
export function isCandidateKey(key) {
|
|
176
|
+
if (!key || typeof key !== 'string') return false;
|
|
177
|
+
if (SKIP_KEYS.has(key) || SKIP_KEYS.has(key.toUpperCase())) return false;
|
|
178
|
+
// skip pure PATH-like / numeric shell junk
|
|
179
|
+
if (key.length < 4 || key.length > 128) return false;
|
|
180
|
+
if (!/^[A-Za-z_][A-Za-z0-9_.-]*$/.test(key)) return false;
|
|
181
|
+
return SECRET_NAME_RE.test(key);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Discover .env* files under a directory (shallow, no node_modules).
|
|
186
|
+
* @param {string} root
|
|
187
|
+
* @param {number} maxDepth
|
|
188
|
+
* @param {string[]} out
|
|
189
|
+
* @param {number} depth
|
|
190
|
+
*/
|
|
191
|
+
function walkEnvFiles(root, maxDepth, out, depth = 0) {
|
|
192
|
+
if (depth > maxDepth || !root) return;
|
|
193
|
+
let entries;
|
|
194
|
+
try {
|
|
195
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
196
|
+
} catch {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
for (const ent of entries) {
|
|
200
|
+
if (ent.name === 'node_modules' || ent.name === '.git' || ent.name === 'dist' || ent.name === 'build') {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const full = path.join(root, ent.name);
|
|
204
|
+
if (ent.isFile() && /^\.env(\.|$)/.test(ent.name)) {
|
|
205
|
+
out.push(full);
|
|
206
|
+
} else if (ent.isDirectory() && !ent.name.startsWith('.') || ent.name === '.agent' || ent.name === '.config') {
|
|
207
|
+
if (ent.name.startsWith('.') && ent.name !== '.agent' && ent.name !== '.config') continue;
|
|
208
|
+
walkEnvFiles(full, maxDepth, out, depth + 1);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Candidate env file paths — discovered, not a hardcoded product list.
|
|
215
|
+
* @param {{ brainDir?: string, projectRoot?: string, cwd?: string, extraFiles?: string[], discoverRoot?: string, maxDepth?: number }} opts
|
|
216
|
+
*/
|
|
217
|
+
export function defaultEnvFilePaths(opts = {}) {
|
|
218
|
+
const home = os.homedir();
|
|
219
|
+
const cwd = opts.cwd || process.cwd();
|
|
220
|
+
const paths = [
|
|
221
|
+
path.join(home, '.env'),
|
|
222
|
+
path.join(home, '.agent', '.env'),
|
|
223
|
+
path.join(home, '.agent', 'skills', 'total-recall', '.env'),
|
|
224
|
+
path.join(home, '.agent', 'skills', 'total-recall', 'config', '.env'),
|
|
225
|
+
path.join(home, '.config', 'total-recall', '.env'),
|
|
226
|
+
path.join(cwd, '.env'),
|
|
227
|
+
path.join(cwd, '.env.local'),
|
|
228
|
+
path.join(cwd, '.env.development'),
|
|
229
|
+
path.join(cwd, '.env.production'),
|
|
230
|
+
];
|
|
231
|
+
if (opts.brainDir) {
|
|
232
|
+
paths.push(path.join(opts.brainDir, '.env'));
|
|
233
|
+
paths.push(path.join(opts.brainDir, 'config', '.env'));
|
|
234
|
+
}
|
|
235
|
+
if (opts.projectRoot) {
|
|
236
|
+
paths.push(path.join(opts.projectRoot, '.env'));
|
|
237
|
+
paths.push(path.join(opts.projectRoot, '.env.local'));
|
|
238
|
+
}
|
|
239
|
+
if (opts.extraFiles && Array.isArray(opts.extraFiles)) {
|
|
240
|
+
paths.push(...opts.extraFiles);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Auto-discover under ~/Github (or discoverRoot) without naming products
|
|
244
|
+
const discoverRoot = opts.discoverRoot ?? path.join(home, 'Github');
|
|
245
|
+
const maxDepth = opts.maxDepth ?? 3;
|
|
246
|
+
if (discoverRoot && fs.existsSync(discoverRoot)) {
|
|
247
|
+
walkEnvFiles(discoverRoot, maxDepth, paths, 0);
|
|
248
|
+
}
|
|
249
|
+
// Also discover under home/.agent
|
|
250
|
+
walkEnvFiles(path.join(home, '.agent'), 4, paths, 0);
|
|
251
|
+
|
|
252
|
+
return [...new Set(paths)];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* @param {Record<string, string>} map
|
|
257
|
+
* @param {string} source
|
|
258
|
+
* @param {Map<string, object>} into
|
|
259
|
+
*/
|
|
260
|
+
function mergeCandidates(map, source, into) {
|
|
261
|
+
for (const [key, value] of Object.entries(map)) {
|
|
262
|
+
if (!isCandidateKey(key)) continue;
|
|
263
|
+
if (!value || !String(value).trim()) continue;
|
|
264
|
+
const existing = into.get(key);
|
|
265
|
+
// Prefer first non-process.env file sources over later process.env unless only env
|
|
266
|
+
if (existing && existing.source !== 'process.env' && source === 'process.env') continue;
|
|
267
|
+
if (existing && existing.source === source) continue;
|
|
268
|
+
into.set(key, {
|
|
269
|
+
key,
|
|
270
|
+
source,
|
|
271
|
+
provider: inferProvider(key),
|
|
272
|
+
masked: maskSecret(value),
|
|
273
|
+
length: String(value).length,
|
|
274
|
+
known: true, // all pattern-matched secrets are first-class; no API whitelist
|
|
275
|
+
// internal only — stripped before API response
|
|
276
|
+
_value: String(value),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Scan process.env + default env files.
|
|
283
|
+
* @param {{ brainDir?: string, projectRoot?: string, cwd?: string, includeProcessEnv?: boolean }} opts
|
|
284
|
+
*/
|
|
285
|
+
export function scanEnvSources(opts = {}) {
|
|
286
|
+
const includeProcessEnv = opts.includeProcessEnv !== false;
|
|
287
|
+
const candidates = new Map();
|
|
288
|
+
|
|
289
|
+
for (const filePath of defaultEnvFilePaths(opts)) {
|
|
290
|
+
try {
|
|
291
|
+
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) continue;
|
|
292
|
+
const text = fs.readFileSync(filePath, 'utf8');
|
|
293
|
+
mergeCandidates(parseEnvText(text), filePath, candidates);
|
|
294
|
+
} catch {
|
|
295
|
+
// unreadable path — skip
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (includeProcessEnv) {
|
|
300
|
+
const envMap = {};
|
|
301
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
302
|
+
if (typeof v === 'string' && v) envMap[k] = v;
|
|
303
|
+
}
|
|
304
|
+
mergeCandidates(envMap, 'process.env', candidates);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const list = [...candidates.values()].sort((a, b) => a.key.localeCompare(b.key));
|
|
308
|
+
return {
|
|
309
|
+
candidates: list,
|
|
310
|
+
sources_scanned: [
|
|
311
|
+
...(includeProcessEnv ? ['process.env'] : []),
|
|
312
|
+
...defaultEnvFilePaths(opts),
|
|
313
|
+
],
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Public scan result (no raw values).
|
|
319
|
+
*/
|
|
320
|
+
export function publicScanResult(scan, existingKeys = new Set()) {
|
|
321
|
+
return {
|
|
322
|
+
candidates: scan.candidates.map(({ key, source, provider, masked, length, known }) => ({
|
|
323
|
+
key,
|
|
324
|
+
source: source === 'process.env' ? 'process.env' : path.basename(source) === source ? source : source,
|
|
325
|
+
source_label: source === 'process.env' ? 'shell environment' : shortenPath(source),
|
|
326
|
+
provider,
|
|
327
|
+
masked,
|
|
328
|
+
length,
|
|
329
|
+
known,
|
|
330
|
+
already_set: existingKeys.has(key),
|
|
331
|
+
})),
|
|
332
|
+
count: scan.candidates.length,
|
|
333
|
+
sources_scanned: scan.sources_scanned.map((s) =>
|
|
334
|
+
s === 'process.env' ? s : shortenPath(s),
|
|
335
|
+
),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function shortenPath(p) {
|
|
340
|
+
const home = os.homedir();
|
|
341
|
+
if (p.startsWith(home)) return `~${p.slice(home.length)}`;
|
|
342
|
+
return p;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Import selected keys into secrets store.
|
|
347
|
+
* @param {string} brainDir
|
|
348
|
+
* @param {{ keys?: string[], pairs?: Record<string,string>, overwrite?: boolean, actor?: string }} opts
|
|
349
|
+
* - keys: import from scan (must re-scan for values)
|
|
350
|
+
* - pairs: explicit key→value (from paste)
|
|
351
|
+
*/
|
|
352
|
+
export async function importEnvSecrets(brainDir, opts = {}) {
|
|
353
|
+
const overwrite = !!opts.overwrite;
|
|
354
|
+
const actor = opts.actor || 'import';
|
|
355
|
+
const existing = await listSecretsMeta(brainDir);
|
|
356
|
+
const existingSet = new Set(existing.map((e) => e.key));
|
|
357
|
+
|
|
358
|
+
/** @type {Record<string, string>} */
|
|
359
|
+
let values = {};
|
|
360
|
+
|
|
361
|
+
if (opts.pairs && typeof opts.pairs === 'object') {
|
|
362
|
+
for (const [k, v] of Object.entries(opts.pairs)) {
|
|
363
|
+
if (isCandidateKey(k) && v) values[k] = String(v);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** @type {Record<string, string>} */
|
|
368
|
+
const sourceByKey = { ...(opts.sourceByKey || {}) };
|
|
369
|
+
|
|
370
|
+
if (Array.isArray(opts.keys) && opts.keys.length) {
|
|
371
|
+
const scan = scanEnvSources({ brainDir, includeProcessEnv: true });
|
|
372
|
+
const byKey = new Map(scan.candidates.map((c) => [c.key, c]));
|
|
373
|
+
for (const k of opts.keys) {
|
|
374
|
+
const c = byKey.get(k);
|
|
375
|
+
if (c?._value) {
|
|
376
|
+
values[k] = c._value;
|
|
377
|
+
sourceByKey[k] = c.source;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// If only "import all" with empty keys and no pairs — import everything from scan
|
|
383
|
+
if (!opts.pairs && (!opts.keys || opts.keys.length === 0) && opts.all) {
|
|
384
|
+
const scan = scanEnvSources({ brainDir, includeProcessEnv: true });
|
|
385
|
+
for (const c of scan.candidates) {
|
|
386
|
+
if (c._value) {
|
|
387
|
+
values[c.key] = c._value;
|
|
388
|
+
sourceByKey[c.key] = c.source;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const imported = [];
|
|
394
|
+
const skipped = [];
|
|
395
|
+
const errors = [];
|
|
396
|
+
|
|
397
|
+
for (const [key, value] of Object.entries(values)) {
|
|
398
|
+
if (existingSet.has(key) && !overwrite) {
|
|
399
|
+
skipped.push({ key, reason: 'already_set' });
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
try {
|
|
403
|
+
const src = sourceByKey[key];
|
|
404
|
+
let repos;
|
|
405
|
+
if (src && typeof src === 'string' && src !== 'process.env' && src !== 'paste') {
|
|
406
|
+
// e.g. .../Github/ultrachat-ai-powered/.env → ultrachat-ai-powered
|
|
407
|
+
const parts = src.replace(/\\/g, '/').split('/');
|
|
408
|
+
const envIdx = parts.findIndex((p) => p.startsWith('.env'));
|
|
409
|
+
if (envIdx > 0) repos = [parts[envIdx - 1]];
|
|
410
|
+
}
|
|
411
|
+
await setSecret(brainDir, key, value, {
|
|
412
|
+
provider: inferProvider(key),
|
|
413
|
+
scope: 'global',
|
|
414
|
+
repos,
|
|
415
|
+
actor,
|
|
416
|
+
});
|
|
417
|
+
imported.push({ key, provider: inferProvider(key), repos });
|
|
418
|
+
existingSet.add(key);
|
|
419
|
+
} catch (err) {
|
|
420
|
+
errors.push({ key, error: err.message });
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
imported,
|
|
426
|
+
skipped,
|
|
427
|
+
errors,
|
|
428
|
+
imported_count: imported.length,
|
|
429
|
+
skipped_count: skipped.length,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Parse pasted .env and return public candidates + internal pairs for import.
|
|
435
|
+
* @param {string} text
|
|
436
|
+
*/
|
|
437
|
+
export function candidatesFromPaste(text) {
|
|
438
|
+
const map = parseEnvText(text);
|
|
439
|
+
const candidates = [];
|
|
440
|
+
const pairs = {};
|
|
441
|
+
for (const [key, value] of Object.entries(map)) {
|
|
442
|
+
if (!isCandidateKey(key) || !value) continue;
|
|
443
|
+
candidates.push({
|
|
444
|
+
key,
|
|
445
|
+
source: 'paste',
|
|
446
|
+
source_label: 'pasted .env',
|
|
447
|
+
provider: inferProvider(key),
|
|
448
|
+
masked: maskSecret(value),
|
|
449
|
+
length: String(value).length,
|
|
450
|
+
known: true,
|
|
451
|
+
});
|
|
452
|
+
pairs[key] = value;
|
|
453
|
+
}
|
|
454
|
+
candidates.sort((a, b) => a.key.localeCompare(b.key));
|
|
455
|
+
return { candidates, pairs };
|
|
456
|
+
}
|
package/src/core/fact-seeker.mjs
CHANGED
|
@@ -115,7 +115,7 @@ export function addToAgenda({ topic, priority = 50, source, rationale = '', tags
|
|
|
115
115
|
if (directInstruction) existing.direct_instruction = true;
|
|
116
116
|
} else {
|
|
117
117
|
agenda.push({
|
|
118
|
-
id: crypto.
|
|
118
|
+
id: crypto.createHash('md5').update(topic).digest('hex').slice(0, 8),
|
|
119
119
|
topic,
|
|
120
120
|
priority,
|
|
121
121
|
source,
|
|
@@ -668,10 +668,10 @@ async function writeAndSurfaceImmediately(topic, synthesis, sourceResults, {
|
|
|
668
668
|
|
|
669
669
|
// Write supporting nodes to vault
|
|
670
670
|
for (const node of supportingNodes) {
|
|
671
|
-
writeNode({ ...node.frontmatter, body: node.body }, vaultDir);
|
|
671
|
+
await writeNode({ ...node.frontmatter, body: node.body }, vaultDir);
|
|
672
672
|
}
|
|
673
673
|
// Write master node to vault
|
|
674
|
-
writeNode({ ...masterNode.frontmatter, body: masterNode.body }, vaultDir);
|
|
674
|
+
await writeNode({ ...masterNode.frontmatter, body: masterNode.body }, vaultDir);
|
|
675
675
|
|
|
676
676
|
for (const sr of sourceResults) {
|
|
677
677
|
try { registerSource(sr, masterNode.slug); } catch (err) { logger.debug('fact-seeker: registerSource fast-path failed', { err: err.message }); }
|
|
@@ -1138,7 +1138,7 @@ Output ONLY valid JSON.`;
|
|
|
1138
1138
|
otherRelated.add(nodeSlug);
|
|
1139
1139
|
otherNode.related = Array.from(otherRelated);
|
|
1140
1140
|
otherNode.updated = new Date().toISOString();
|
|
1141
|
-
writeNode(otherNode, vaultDir);
|
|
1141
|
+
await writeNode(otherNode, vaultDir);
|
|
1142
1142
|
}
|
|
1143
1143
|
}
|
|
1144
1144
|
}
|
|
@@ -1159,7 +1159,7 @@ Output ONLY valid JSON.`;
|
|
|
1159
1159
|
otherContradicts.add(nodeSlug);
|
|
1160
1160
|
otherNode.contradicts = Array.from(otherContradicts);
|
|
1161
1161
|
otherNode.updated = new Date().toISOString();
|
|
1162
|
-
writeNode(otherNode, vaultDir);
|
|
1162
|
+
await writeNode(otherNode, vaultDir);
|
|
1163
1163
|
}
|
|
1164
1164
|
}
|
|
1165
1165
|
}
|
|
@@ -1178,7 +1178,7 @@ Output ONLY valid JSON.`;
|
|
|
1178
1178
|
if (otherNode) {
|
|
1179
1179
|
otherNode.superseded_by = nodeSlug;
|
|
1180
1180
|
otherNode.updated = new Date().toISOString();
|
|
1181
|
-
writeNode(otherNode, vaultDir);
|
|
1181
|
+
await writeNode(otherNode, vaultDir);
|
|
1182
1182
|
}
|
|
1183
1183
|
}
|
|
1184
1184
|
}
|
|
@@ -1200,7 +1200,7 @@ Output ONLY valid JSON.`;
|
|
|
1200
1200
|
updated: new Date().toISOString()
|
|
1201
1201
|
};
|
|
1202
1202
|
|
|
1203
|
-
writeNode(updatedNode, vaultDir);
|
|
1203
|
+
await writeNode(updatedNode, vaultDir);
|
|
1204
1204
|
|
|
1205
1205
|
const conclusions = [];
|
|
1206
1206
|
if (result.insights && result.insights.length > 0) {
|
|
@@ -1221,7 +1221,7 @@ Output ONLY valid JSON.`;
|
|
|
1221
1221
|
|
|
1222
1222
|
if (result.synthesized_instructions && result.synthesized_instructions.length > 0) {
|
|
1223
1223
|
for (const inst of result.synthesized_instructions) {
|
|
1224
|
-
const ruleSlug = `rule-synth-${crypto.
|
|
1224
|
+
const ruleSlug = `rule-synth-${crypto.createHash('md5').update(inst.title || inst.content || '').digest('hex').slice(0, 10)}`;
|
|
1225
1225
|
const ruleNode = {
|
|
1226
1226
|
type: 'memory',
|
|
1227
1227
|
slug: ruleSlug,
|
|
@@ -1267,7 +1267,7 @@ Output ONLY valid JSON.`;
|
|
|
1267
1267
|
body: inst.body || ''
|
|
1268
1268
|
};
|
|
1269
1269
|
|
|
1270
|
-
writeNode(ruleNode, vaultDir);
|
|
1270
|
+
await writeNode(ruleNode, vaultDir);
|
|
1271
1271
|
rulesSynthesized = true;
|
|
1272
1272
|
logger.info({
|
|
1273
1273
|
subsystem: 'fact-seeker',
|
|
@@ -1301,7 +1301,7 @@ Output ONLY valid JSON.`;
|
|
|
1301
1301
|
const queueDir = path.join(localBrainDir, 'scheduler', 'queue');
|
|
1302
1302
|
const { persistTaskToDisk } = await import('./scheduler.mjs');
|
|
1303
1303
|
for (const t of result.autonomous_tasks) {
|
|
1304
|
-
const taskSlug = `delib-task-${crypto.
|
|
1304
|
+
const taskSlug = `delib-task-${crypto.createHash('md5').update(t.task || t.topic || t.description || '').digest('hex').slice(0, 8)}`;
|
|
1305
1305
|
const taskNode = {
|
|
1306
1306
|
type: 'task',
|
|
1307
1307
|
slug: taskSlug,
|
|
@@ -1389,7 +1389,7 @@ Output ONLY the raw markdown of the polished body, starting immediately with no
|
|
|
1389
1389
|
updated: new Date().toISOString()
|
|
1390
1390
|
};
|
|
1391
1391
|
|
|
1392
|
-
writeNode(updatedNode, vaultDir);
|
|
1392
|
+
await writeNode(updatedNode, vaultDir);
|
|
1393
1393
|
|
|
1394
1394
|
return {
|
|
1395
1395
|
success: true,
|
|
@@ -1486,7 +1486,7 @@ Output ONLY valid JSON.`;
|
|
|
1486
1486
|
updated: new Date().toISOString()
|
|
1487
1487
|
};
|
|
1488
1488
|
|
|
1489
|
-
writeNode(updatedNode, vaultDir);
|
|
1489
|
+
await writeNode(updatedNode, vaultDir);
|
|
1490
1490
|
|
|
1491
1491
|
// Trigger immediate surface recompilation if active rules/sources were added
|
|
1492
1492
|
if (skillsDir && derivedDir && instructionsFile) {
|
|
@@ -1578,7 +1578,7 @@ Output ONLY valid JSON.`;
|
|
|
1578
1578
|
|
|
1579
1579
|
// Also schedule a scheduler task to run proactive research for this topic
|
|
1580
1580
|
const prio = tangent.priority === 'high' ? 65 : (tangent.priority === 'low' ? 45 : 55);
|
|
1581
|
-
const taskSlug = `proactive-research-${crypto.
|
|
1581
|
+
const taskSlug = `proactive-research-${crypto.createHash('md5').update(tangent.topic || '').digest('hex').slice(0, 8)}`;
|
|
1582
1582
|
const taskNode = {
|
|
1583
1583
|
type: 'task',
|
|
1584
1584
|
slug: taskSlug,
|
|
@@ -1618,7 +1618,7 @@ Output ONLY valid JSON.`;
|
|
|
1618
1618
|
updated: new Date().toISOString()
|
|
1619
1619
|
};
|
|
1620
1620
|
|
|
1621
|
-
writeNode(updatedNode, vaultDir);
|
|
1621
|
+
await writeNode(updatedNode, vaultDir);
|
|
1622
1622
|
|
|
1623
1623
|
return {
|
|
1624
1624
|
success: true,
|
|
@@ -208,7 +208,7 @@ export function detectRuleFiles(dirs) {
|
|
|
208
208
|
* @param {{ force?: boolean, vaultDir?: string }} opts
|
|
209
209
|
* @returns {{ imported: object[], skipped: object[], failed: object[] }}
|
|
210
210
|
*/
|
|
211
|
-
export function importRuleFiles(files, { force = false, vaultDir = VAULT_DIR } = {}) {
|
|
211
|
+
export async function importRuleFiles(files, { force = false, vaultDir = VAULT_DIR } = {}) {
|
|
212
212
|
const imported = [], skipped = [], failed = [];
|
|
213
213
|
|
|
214
214
|
for (const file of files) {
|
|
@@ -225,23 +225,35 @@ export function importRuleFiles(files, { force = false, vaultDir = VAULT_DIR } =
|
|
|
225
225
|
type: 'memory',
|
|
226
226
|
slug: file.slug,
|
|
227
227
|
title: `Imported rules: ${file.filename}`,
|
|
228
|
-
|
|
228
|
+
description: `Imported IDE rules from ${file.filename}`,
|
|
229
|
+
timestamp: new Date().toISOString(),
|
|
230
|
+
// Package memory.category enum — host folder preserved as tag folder:instructions
|
|
231
|
+
category: 'preferences',
|
|
229
232
|
status: 'active',
|
|
230
233
|
confidence: 1.0,
|
|
231
234
|
importance: 5,
|
|
232
|
-
|
|
235
|
+
schema_version: 2,
|
|
236
|
+
modality: 'must',
|
|
237
|
+
subject: 'agent',
|
|
238
|
+
predicate: 'follow',
|
|
239
|
+
object: 'imported-rules',
|
|
240
|
+
sentiment_polarity: 'directive_must',
|
|
241
|
+
sentiment_target: 'agent',
|
|
242
|
+
tags: ['imported-rules', 'folder:instructions', file.ide, path.basename(file.filename)].filter(Boolean),
|
|
233
243
|
created: new Date().toISOString(),
|
|
234
244
|
updated: new Date().toISOString(),
|
|
235
245
|
last_accessed: new Date().toISOString(),
|
|
236
246
|
source: {
|
|
237
247
|
type: 'import',
|
|
248
|
+
session_id: `import-${file.slug}`,
|
|
249
|
+
evidence_count: 1,
|
|
238
250
|
file: file.absolutePath,
|
|
239
251
|
imported_at: new Date().toISOString(),
|
|
240
252
|
},
|
|
241
253
|
body: content,
|
|
242
254
|
};
|
|
243
255
|
|
|
244
|
-
writeNode(node, vaultDir);
|
|
256
|
+
await writeNode(node, vaultDir);
|
|
245
257
|
imported.push({ slug: node.slug, title: node.title, file: file.absolutePath, size: file.size });
|
|
246
258
|
} catch (err) {
|
|
247
259
|
failed.push({ ...file, error: err.message });
|
|
@@ -256,7 +268,7 @@ export function importRuleFiles(files, { force = false, vaultDir = VAULT_DIR } =
|
|
|
256
268
|
*
|
|
257
269
|
* @param {{ dirs?: string[], force?: boolean, vaultDir?: string, dryRun?: boolean }} opts
|
|
258
270
|
*/
|
|
259
|
-
export function detectAndImport({ dirs, force = false, vaultDir = VAULT_DIR, dryRun = false } = {}) {
|
|
271
|
+
export async function detectAndImport({ dirs, force = false, vaultDir = VAULT_DIR, dryRun = false } = {}) {
|
|
260
272
|
const detected = detectRuleFiles(dirs);
|
|
261
273
|
const toImport = detected.filter(f => !f.alreadyImported || force);
|
|
262
274
|
|
|
@@ -264,7 +276,7 @@ export function detectAndImport({ dirs, force = false, vaultDir = VAULT_DIR, dry
|
|
|
264
276
|
return { detected, toImport, imported: [], skipped: detected.filter(f => f.alreadyImported && !force), failed: [], dryRun: true };
|
|
265
277
|
}
|
|
266
278
|
|
|
267
|
-
const { imported, skipped, failed } = importRuleFiles(toImport, { force, vaultDir });
|
|
279
|
+
const { imported, skipped, failed } = await importRuleFiles(toImport, { force, vaultDir });
|
|
268
280
|
return { detected, imported, skipped, failed, dryRun: false };
|
|
269
281
|
}
|
|
270
282
|
|
|
@@ -278,7 +290,8 @@ function makeSlug(filename, absPath) {
|
|
|
278
290
|
}
|
|
279
291
|
|
|
280
292
|
function healSymlinkTarget(target) {
|
|
281
|
-
|
|
293
|
+
// Generic heal only — never special-case product/repo path segments
|
|
294
|
+
let healed = target;
|
|
282
295
|
if (fs.existsSync(healed)) return healed;
|
|
283
296
|
|
|
284
297
|
const skillMatch = healed.match(/\/(\.agents?)\/skills\/([^\/]+)\/(SKILL\.md)$/i);
|
|
@@ -147,7 +147,7 @@ export async function runInferenceTask(slugs, { vaultDir, inboxDir, runtimeConfi
|
|
|
147
147
|
// ─── Node Writers ───────────────────────────────────────────────────────────────
|
|
148
148
|
|
|
149
149
|
function writeConclusionNode(inboxDir, conclusion, sourceSlugList, vaultDir) {
|
|
150
|
-
const slug = `inference-${crypto.
|
|
150
|
+
const slug = `inference-${crypto.createHash('md5').update(conclusion.title || conclusion.description || JSON.stringify(conclusion)).digest('hex').slice(0, 8)}`;
|
|
151
151
|
const now = new Date().toISOString();
|
|
152
152
|
|
|
153
153
|
let latestPublished = null;
|
|
@@ -239,7 +239,7 @@ function writeContradictionRecord(conflictsDir, contradiction) {
|
|
|
239
239
|
fs.mkdirSync(conflictsDir, { recursive: true });
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
const slug = `contradiction-${crypto.
|
|
242
|
+
const slug = `contradiction-${crypto.createHash('md5').update(contradiction.node_a + contradiction.node_b).digest('hex').slice(0, 8)}`;
|
|
243
243
|
const now = new Date().toISOString();
|
|
244
244
|
|
|
245
245
|
const data = {
|
|
@@ -264,7 +264,7 @@ function writeMergeProposal(proposalsDir, candidate) {
|
|
|
264
264
|
fs.mkdirSync(proposalsDir, { recursive: true });
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
-
const slug = `merge-proposal-${crypto.
|
|
267
|
+
const slug = `merge-proposal-${crypto.createHash('md5').update(candidate.node_a + candidate.node_b).digest('hex').slice(0, 8)}`;
|
|
268
268
|
const now = new Date().toISOString();
|
|
269
269
|
|
|
270
270
|
const data = {
|