@causyne/agent 0.0.0-stage → 1.10.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/LICENSE +102 -0
- package/README.md +31 -2
- package/SECURITY.md +24 -0
- package/bin/causyne.js +13 -0
- package/docs/ARCHITECTURE.md +63 -0
- package/docs/PROVENANCE.md +29 -0
- package/docs/SECURITY_REVIEW.md +89 -0
- package/docs/THIRD_PARTY_NOTICES.md +49 -0
- package/native-skills/ai-engineering/context-window-management/SKILL.md +43 -0
- package/native-skills/ai-engineering/llm-evaluation/SKILL.md +46 -0
- package/native-skills/ai-engineering/model-cost-and-latency/SKILL.md +46 -0
- package/native-skills/ai-engineering/prompt-construction/SKILL.md +42 -0
- package/native-skills/ai-engineering/retrieval-augmented-generation/SKILL.md +42 -0
- package/native-skills/ai-engineering/structured-output-and-schemas/SKILL.md +45 -0
- package/native-skills/ai-engineering/tool-calling-agents/SKILL.md +43 -0
- package/native-skills/android/app-performance-profiling/SKILL.md +42 -0
- package/native-skills/android/background-work-scheduling/SKILL.md +47 -0
- package/native-skills/android/compose-state-and-recomposition/SKILL.md +43 -0
- package/native-skills/android/gradle-build-configuration/SKILL.md +41 -0
- package/native-skills/android/lifecycle-and-leaks/SKILL.md +46 -0
- package/native-skills/android/play-store-release/SKILL.md +43 -0
- package/native-skills/android/room-and-persistence/SKILL.md +40 -0
- package/native-skills/backend/authentication-and-sessions/SKILL.md +45 -0
- package/native-skills/backend/background-jobs-and-queues/SKILL.md +42 -0
- package/native-skills/backend/caching-strategy/SKILL.md +44 -0
- package/native-skills/backend/graceful-shutdown/SKILL.md +42 -0
- package/native-skills/backend/rate-limiting-and-quotas/SKILL.md +40 -0
- package/native-skills/backend/rest-api-design/SKILL.md +40 -0
- package/native-skills/backend/webhook-delivery-and-receipt/SKILL.md +43 -0
- package/native-skills/cloud/cost-optimisation/SKILL.md +46 -0
- package/native-skills/cloud/iam-least-privilege/SKILL.md +43 -0
- package/native-skills/cloud/infrastructure-as-code/SKILL.md +43 -0
- package/native-skills/cloud/multi-region-and-failover/SKILL.md +43 -0
- package/native-skills/cloud/serverless-patterns/SKILL.md +47 -0
- package/native-skills/data/data-quality-monitoring/SKILL.md +51 -0
- package/native-skills/data/etl-pipeline-reliability/SKILL.md +43 -0
- package/native-skills/data/exploratory-analysis/SKILL.md +43 -0
- package/native-skills/data/metric-definition/SKILL.md +42 -0
- package/native-skills/data/notebook-to-production/SKILL.md +43 -0
- package/native-skills/database/backup-and-recovery/SKILL.md +43 -0
- package/native-skills/database/connection-pooling/SKILL.md +42 -0
- package/native-skills/database/data-integrity-and-constraints/SKILL.md +42 -0
- package/native-skills/database/query-optimisation/SKILL.md +38 -0
- package/native-skills/database/safe-migrations/SKILL.md +47 -0
- package/native-skills/database/schema-design/SKILL.md +38 -0
- package/native-skills/database/transactions-and-isolation/SKILL.md +41 -0
- package/native-skills/design/colour-system-construction/SKILL.md +38 -0
- package/native-skills/design/design-direction-brief/SKILL.md +41 -0
- package/native-skills/design/motion-and-transitions/SKILL.md +38 -0
- package/native-skills/design/responsive-strategy/SKILL.md +39 -0
- package/native-skills/design/spacing-and-rhythm/SKILL.md +38 -0
- package/native-skills/design/type-scale-and-hierarchy/SKILL.md +39 -0
- package/native-skills/design/visual-qa-loop/SKILL.md +42 -0
- package/native-skills/devops/docker-multi-stage-builds/SKILL.md +60 -0
- package/native-skills/devops/github-actions-ci/SKILL.md +60 -0
- package/native-skills/devops/incident-triage/SKILL.md +38 -0
- package/native-skills/devops/kubernetes-deployment-hardening/SKILL.md +64 -0
- package/native-skills/devops/observability-baseline/SKILL.md +38 -0
- package/native-skills/devops/secret-management/SKILL.md +40 -0
- package/native-skills/devops/terraform-state-and-modules/SKILL.md +36 -0
- package/native-skills/devops/zero-downtime-deploys/SKILL.md +40 -0
- package/native-skills/git/bisect-regression-hunt/SKILL.md +43 -0
- package/native-skills/git/branch-strategy/SKILL.md +39 -0
- package/native-skills/git/commit-hygiene/SKILL.md +41 -0
- package/native-skills/git/history-recovery/SKILL.md +44 -0
- package/native-skills/git/large-repository-surgery/SKILL.md +46 -0
- package/native-skills/git/merge-conflict-resolution/SKILL.md +42 -0
- package/native-skills/media/cinematic-3d-shot/SKILL.md +104 -0
- package/native-skills/mobile/app-startup-optimisation/SKILL.md +41 -0
- package/native-skills/mobile/cross-platform-tradeoffs/SKILL.md +42 -0
- package/native-skills/mobile/deep-linking/SKILL.md +39 -0
- package/native-skills/mobile/mobile-storage-security/SKILL.md +45 -0
- package/native-skills/mobile/offline-first-sync/SKILL.md +42 -0
- package/native-skills/mobile/push-notifications/SKILL.md +38 -0
- package/native-skills/performance/algorithmic-complexity/SKILL.md +43 -0
- package/native-skills/performance/concurrency-and-parallelism/SKILL.md +42 -0
- package/native-skills/performance/load-testing/SKILL.md +42 -0
- package/native-skills/performance/memory-and-leak-hunting/SKILL.md +41 -0
- package/native-skills/performance/network-and-payload-efficiency/SKILL.md +42 -0
- package/native-skills/performance/profiling-methodology/SKILL.md +46 -0
- package/native-skills/python/async-correctness/SKILL.md +40 -0
- package/native-skills/python/cli-tool-construction/SKILL.md +44 -0
- package/native-skills/python/data-processing-pipelines/SKILL.md +38 -0
- package/native-skills/python/environment-isolation/SKILL.md +41 -0
- package/native-skills/python/packaging-and-distribution/SKILL.md +56 -0
- package/native-skills/python/pytest-suite-design/SKILL.md +39 -0
- package/native-skills/python/type-hints-and-static-checking/SKILL.md +42 -0
- package/native-skills/security/authorization-model/SKILL.md +42 -0
- package/native-skills/security/crypto-usage/SKILL.md +48 -0
- package/native-skills/security/dependency-supply-chain/SKILL.md +42 -0
- package/native-skills/security/input-validation-and-injection/SKILL.md +44 -0
- package/native-skills/security/secure-file-uploads/SKILL.md +46 -0
- package/native-skills/security/security-headers-and-csp/SKILL.md +44 -0
- package/native-skills/security/threat-modelling/SKILL.md +46 -0
- package/native-skills/testing/end-to-end-test-stability/SKILL.md +42 -0
- package/native-skills/testing/property-based-testing/SKILL.md +46 -0
- package/native-skills/testing/regression-test-from-a-bug/SKILL.md +41 -0
- package/native-skills/testing/snapshot-and-visual-regression/SKILL.md +42 -0
- package/native-skills/testing/test-doubles-and-mocking/SKILL.md +43 -0
- package/native-skills/testing/test-strategy-and-pyramid/SKILL.md +39 -0
- package/native-skills/testing/writing-good-assertions/SKILL.md +42 -0
- package/native-skills/web/accessible-components/SKILL.md +45 -0
- package/native-skills/web/api-client-resilience/SKILL.md +39 -0
- package/native-skills/web/bundle-size-reduction/SKILL.md +38 -0
- package/native-skills/web/core-web-vitals/SKILL.md +40 -0
- package/native-skills/web/css-layout-systems/SKILL.md +41 -0
- package/native-skills/web/form-validation-ux/SKILL.md +39 -0
- package/native-skills/web/react-state-architecture/SKILL.md +41 -0
- package/native-skills/web/ssr-hydration-correctness/SKILL.md +45 -0
- package/package.json +68 -4
- package/src/agentLoop.js +2458 -0
- package/src/appChannel.js +109 -0
- package/src/approvalGrants.js +66 -0
- package/src/auditLog.js +52 -0
- package/src/autoUpdate.js +12 -0
- package/src/capabilities.js +36 -0
- package/src/checkpoints.js +1199 -0
- package/src/cliMode.js +638 -0
- package/src/commands.js +805 -0
- package/src/compiledSkills.js +540 -0
- package/src/config.js +130 -0
- package/src/contextManager.js +498 -0
- package/src/cortex/autoCondenser.js +192 -0
- package/src/cortex/provisioner.js +27 -0
- package/src/cortex/vectorDb.js +447 -0
- package/src/designPalettes.js +99 -0
- package/src/devMode.js +6 -0
- package/src/embodimentSession.js +180 -0
- package/src/entrypoint.js +194 -0
- package/src/execSafe.js +21 -0
- package/src/gitSnapshot.js +378 -0
- package/src/mcp/jsonRpc.js +165 -0
- package/src/mcp/mcpClient.js +136 -0
- package/src/mcp/mcpRegistry.js +207 -0
- package/src/memory.js +62 -0
- package/src/memoryManager.js +124 -0
- package/src/modelCatalogue.js +132 -0
- package/src/modelIds.js +42 -0
- package/src/modelRouter.js +145 -0
- package/src/outputStore.js +246 -0
- package/src/perception.js +124 -0
- package/src/planCache.js +437 -0
- package/src/procSignal.js +32 -0
- package/src/providers/anthropic.js +408 -0
- package/src/providers/base.js +328 -0
- package/src/providers/dashscope-byok.js +86 -0
- package/src/providers/deepseek.js +80 -0
- package/src/providers/factory.js +37 -0
- package/src/providers/gemini.js +346 -0
- package/src/providers/moonshot.js +73 -0
- package/src/providers/muse.js +169 -0
- package/src/providers/ollama.js +104 -0
- package/src/providers/openai-compatible.js +84 -0
- package/src/providers/openai.js +20 -0
- package/src/providers/qwen-byok.js +76 -0
- package/src/providers/xai.js +73 -0
- package/src/provisioning/capabilityConsent.js +24 -0
- package/src/provisioning/capabilityProvisioner.js +271 -0
- package/src/provisioning/capabilityTools.js +29 -0
- package/src/reporting.js +373 -0
- package/src/sandbox.js +529 -0
- package/src/sessionStore.js +279 -0
- package/src/skillsManager.js +288 -0
- package/src/slopDetector.js +307 -0
- package/src/taskBuffer.js +45 -0
- package/src/taskShape.js +241 -0
- package/src/textDiff.js +150 -0
- package/src/tokenTracker.js +560 -0
- package/src/tool-scripts/ax_snapshot.applescript +39 -0
- package/src/tool-scripts/ax_snapshot.ps1 +10 -0
- package/src/tool-scripts/desktop_act.applescript +58 -0
- package/src/tool-scripts/resolve_automator.py +119 -0
- package/src/toolPacks.js +524 -0
- package/src/tools/archive.js +63 -0
- package/src/tools/astryxOracle.js +78 -0
- package/src/tools/axSnapshot.js +165 -0
- package/src/tools/blenderAutomator.js +349 -0
- package/src/tools/browserAutomator.js +252 -0
- package/src/tools/checkSystemSpecs.js +64 -0
- package/src/tools/clipboard.js +36 -0
- package/src/tools/codeSearchAst.js +101 -0
- package/src/tools/codeSearcher.js +230 -0
- package/src/tools/codeTools.js +262 -0
- package/src/tools/codebaseSearch.js +75 -0
- package/src/tools/createSkill.js +129 -0
- package/src/tools/deferredMcpSearch.js +246 -0
- package/src/tools/desktopAct.js +208 -0
- package/src/tools/desktopOpen.js +34 -0
- package/src/tools/dynamicLoader.js +44 -0
- package/src/tools/executeProceduralMedia.js +93 -0
- package/src/tools/fsDelete.js +51 -0
- package/src/tools/fsEdit.js +105 -0
- package/src/tools/fsFind.js +128 -0
- package/src/tools/fsList.js +77 -0
- package/src/tools/fsMove.js +57 -0
- package/src/tools/fsRead.js +87 -0
- package/src/tools/fsSearch.js +198 -0
- package/src/tools/fsWrite.js +41 -0
- package/src/tools/gameAiBehaviorTree.js +208 -0
- package/src/tools/gameAnimationRig.js +194 -0
- package/src/tools/gameAssetFetcher.js +372 -0
- package/src/tools/gameAssetHarvester.js +210 -0
- package/src/tools/gameAudioSynth.js +267 -0
- package/src/tools/gameBuilder.js +886 -0
- package/src/tools/gameCanvasAudit.js +101 -0
- package/src/tools/gameCinematicSequencer.js +151 -0
- package/src/tools/gameDataForge.js +124 -0
- package/src/tools/gameDialogueTreeGen.js +166 -0
- package/src/tools/gameEconomyBalancer.js +161 -0
- package/src/tools/gameEntitySpawner.js +207 -0
- package/src/tools/gameLoreForge.js +153 -0
- package/src/tools/gameQuestArcBuilder.js +152 -0
- package/src/tools/gameSceneBuilder.js +158 -0
- package/src/tools/gameShaderForge.js +181 -0
- package/src/tools/gameTilemapGenerator.js +164 -0
- package/src/tools/gameUiHudBuilder.js +292 -0
- package/src/tools/gameVfxEmitterGen.js +108 -0
- package/src/tools/gameWebRuntimeInspector.js +91 -0
- package/src/tools/generateMediaViaApi.js +358 -0
- package/src/tools/gitExec.js +54 -0
- package/src/tools/godotAutomator.js +437 -0
- package/src/tools/manageLocalAiDependencies.js +220 -0
- package/src/tools/memoryWrite.js +99 -0
- package/src/tools/multiEdit.js +107 -0
- package/src/tools/palmierAutomator.js +104 -0
- package/src/tools/pdfReader.js +112 -0
- package/src/tools/persistentShell.js +148 -0
- package/src/tools/printer.js +316 -0
- package/src/tools/procTerm.js +91 -0
- package/src/tools/processList.js +52 -0
- package/src/tools/readNativeSkill.js +39 -0
- package/src/tools/registry.js +277 -0
- package/src/tools/resolveAutomator.js +102 -0
- package/src/tools/screenshot.js +75 -0
- package/src/tools/subagentOrchestrator.js +305 -0
- package/src/tools/sysRun.js +125 -0
- package/src/tools/todoWrite.js +75 -0
- package/src/tools/toolDispatch.js +127 -0
- package/src/tools/toolForge.js +124 -0
- package/src/tools/updatePlan.js +88 -0
- package/src/tools/viewImage.js +82 -0
- package/src/tools/visionInspector.js +154 -0
- package/src/tools/webFetch.js +74 -0
- package/src/tools/webSearch.js +82 -0
- package/src/tui/ask.js +164 -0
- package/src/tui/banner.js +158 -0
- package/src/tui/block.js +152 -0
- package/src/tui/chatRenderer.js +953 -0
- package/src/tui/completion.js +424 -0
- package/src/tui/dialogs.js +606 -0
- package/src/tui/diff.js +296 -0
- package/src/tui/history.js +135 -0
- package/src/tui/home.js +217 -0
- package/src/tui/invocation.js +105 -0
- package/src/tui/keys.js +443 -0
- package/src/tui/lineEditor.js +256 -0
- package/src/tui/overlay.js +256 -0
- package/src/tui/prompt.js +1258 -0
- package/src/tui/selectList.js +335 -0
- package/src/tui/stage.js +330 -0
- package/src/tui/stageSurface.js +609 -0
- package/src/tui/statusLine.js +254 -0
- package/src/tui/text.js +345 -0
- package/src/tui/theme.js +393 -0
- package/src/tui/toolDisplay.js +176 -0
- package/src/tui/transcript.js +235 -0
- package/src/tui/viewport.js +337 -0
- package/src/uncertainty.js +271 -0
- package/src/userSkills.js +265 -0
- package/src/utils/cli_ui.js +0 -0
- package/src/utils/inquirer.js +12 -0
- package/src/utils/paths.js +37 -0
- package/src/version.js +38 -0
- package/src/vfeTracker.js +220 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import crypto from 'crypto';
|
|
5
|
+
import { causyneHome } from './utils/paths.js';
|
|
6
|
+
|
|
7
|
+
// Chat sessions. One conversation, one file, scoped to one workspace.
|
|
8
|
+
//
|
|
9
|
+
// WHAT THIS REPLACES, AND WHY IT HAD TO GO.
|
|
10
|
+
//
|
|
11
|
+
// There was exactly one `~/.causyne/memory/chat_history.json`. No id, no
|
|
12
|
+
// workspace key, no time bound. Every invocation of Causyne, in every directory,
|
|
13
|
+
// on every project, loaded the SAME conversation. So:
|
|
14
|
+
//
|
|
15
|
+
// work on Causyne in ~/Causyne -> history saved
|
|
16
|
+
// weeks later, open ~/Desktop/3d -> the SAME history loads
|
|
17
|
+
// type "continue" -> the agent resumes the Causyne
|
|
18
|
+
// conversation, in the Blender directory,
|
|
19
|
+
// and goes scanning the old project
|
|
20
|
+
//
|
|
21
|
+
// That is not a memory feature working. It is one unbounded conversation
|
|
22
|
+
// impersonating a memory, and it gets worse every day it is used: context from
|
|
23
|
+
// months-old work leaks into today's task, and work from one client's project
|
|
24
|
+
// leaks into another's.
|
|
25
|
+
//
|
|
26
|
+
// THE MODEL, taken from opencode's SessionV2.Info (id, projectID, title,
|
|
27
|
+
// time.created/updated/archived): a session belongs to a WORKSPACE and is
|
|
28
|
+
// resumed only when it is both the same workspace and recent. A different
|
|
29
|
+
// directory is a different conversation. An old conversation in the same
|
|
30
|
+
// directory is still there — listed, switchable — but it does not silently
|
|
31
|
+
// become the thing "continue" continues.
|
|
32
|
+
|
|
33
|
+
const SESSION_DIR = path.join(causyneHome(), 'sessions');
|
|
34
|
+
const LEGACY_HISTORY = path.join(causyneHome(), 'memory', 'chat_history.json');
|
|
35
|
+
const POINTER_FILE = path.join(SESSION_DIR, 'active.json');
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* How long a session stays resumable in its own workspace.
|
|
39
|
+
*
|
|
40
|
+
* Past this, returning to the directory starts a NEW conversation rather than
|
|
41
|
+
* reviving one whose subject you have forgotten. The old one is still listed
|
|
42
|
+
* and can be resumed by name — the point is that it stops being the default.
|
|
43
|
+
*/
|
|
44
|
+
const RESUME_WINDOW_MS = 12 * 60 * 60 * 1000;
|
|
45
|
+
|
|
46
|
+
/** Hard ceiling on retained sessions. The oldest are archived out first. */
|
|
47
|
+
const MAX_SESSIONS = 50;
|
|
48
|
+
|
|
49
|
+
function ensureDir() {
|
|
50
|
+
if (!fs.existsSync(SESSION_DIR)) fs.mkdirSync(SESSION_DIR, { recursive: true, mode: 0o700 });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A stable, filesystem-safe key for a workspace path. */
|
|
54
|
+
function workspaceKey(workspace) {
|
|
55
|
+
return crypto.createHash('sha256').update(String(workspace || '')).digest('hex').slice(0, 12);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function sessionFile(id) {
|
|
59
|
+
const safe = String(id || '').replace(/[^a-zA-Z0-9_-]/g, '');
|
|
60
|
+
return path.join(SESSION_DIR, `${safe}.json`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A short human title from the first thing the user actually asked for.
|
|
65
|
+
*
|
|
66
|
+
* `chat_history.json` had no title at all, which is why nothing could ever be
|
|
67
|
+
* listed or chosen. A session you cannot name is a session you cannot leave.
|
|
68
|
+
*/
|
|
69
|
+
export function titleFrom(text) {
|
|
70
|
+
const clean = String(text || '').replace(/\s+/g, ' ').trim();
|
|
71
|
+
if (!clean) return 'Untitled';
|
|
72
|
+
return clean.length > 60 ? clean.slice(0, 57) + '…' : clean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readSession(id) {
|
|
76
|
+
try {
|
|
77
|
+
const file = sessionFile(id);
|
|
78
|
+
if (!fs.existsSync(file)) return null;
|
|
79
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
80
|
+
} catch (e) {
|
|
81
|
+
return null; // a corrupt session degrades to "no session", never to a crash
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function writeSession(session) {
|
|
86
|
+
try {
|
|
87
|
+
ensureDir();
|
|
88
|
+
fs.writeFileSync(sessionFile(session.id), JSON.stringify(session, null, 2), { mode: 0o600 });
|
|
89
|
+
return true;
|
|
90
|
+
} catch (e) {
|
|
91
|
+
return false; // fail open: losing a session must not fail the task
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Every session, newest first. Messages are NOT loaded — this is the index. */
|
|
96
|
+
export function listSessions() {
|
|
97
|
+
try {
|
|
98
|
+
ensureDir();
|
|
99
|
+
const rows = [];
|
|
100
|
+
for (const name of fs.readdirSync(SESSION_DIR)) {
|
|
101
|
+
if (!name.endsWith('.json') || name === 'active.json') continue;
|
|
102
|
+
const session = readSession(name.replace(/\.json$/, ''));
|
|
103
|
+
if (!session) continue;
|
|
104
|
+
rows.push({
|
|
105
|
+
id: session.id,
|
|
106
|
+
title: session.title || 'Untitled',
|
|
107
|
+
workspace: session.workspace || '',
|
|
108
|
+
createdAt: session.createdAt || 0,
|
|
109
|
+
updatedAt: session.updatedAt || 0,
|
|
110
|
+
messageCount: Array.isArray(session.messages) ? session.messages.length : 0
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return rows.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
114
|
+
} catch (e) {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Retire the oldest sessions past MAX_SESSIONS.
|
|
121
|
+
*
|
|
122
|
+
* Called at session resolution rather than on a timer — nothing may run in the
|
|
123
|
+
* background (ZM-01), and a session boundary is often enough for a ceiling
|
|
124
|
+
* measured in dozens.
|
|
125
|
+
*/
|
|
126
|
+
export function pruneSessions(max = MAX_SESSIONS) {
|
|
127
|
+
try {
|
|
128
|
+
const all = listSessions();
|
|
129
|
+
let removed = 0;
|
|
130
|
+
for (const row of all.slice(max)) {
|
|
131
|
+
try { fs.unlinkSync(sessionFile(row.id)); removed++; } catch (e) { /* skip */ }
|
|
132
|
+
}
|
|
133
|
+
return removed;
|
|
134
|
+
} catch (e) {
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function createSession(workspace, title = 'Untitled') {
|
|
140
|
+
const now = Date.now();
|
|
141
|
+
const session = {
|
|
142
|
+
id: `s_${now.toString(36)}_${crypto.randomBytes(3).toString('hex')}`,
|
|
143
|
+
workspace: String(workspace || ''),
|
|
144
|
+
workspaceKey: workspaceKey(workspace),
|
|
145
|
+
title,
|
|
146
|
+
createdAt: now,
|
|
147
|
+
updatedAt: now,
|
|
148
|
+
messages: []
|
|
149
|
+
};
|
|
150
|
+
writeSession(session);
|
|
151
|
+
setActiveSession(session.id);
|
|
152
|
+
pruneSessions();
|
|
153
|
+
return session;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function getActiveSessionId() {
|
|
157
|
+
try {
|
|
158
|
+
if (!fs.existsSync(POINTER_FILE)) return null;
|
|
159
|
+
return JSON.parse(fs.readFileSync(POINTER_FILE, 'utf8')).id || null;
|
|
160
|
+
} catch (e) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function setActiveSession(id) {
|
|
166
|
+
try {
|
|
167
|
+
ensureDir();
|
|
168
|
+
fs.writeFileSync(POINTER_FILE, JSON.stringify({ id, at: Date.now() }, null, 2), { mode: 0o600 });
|
|
169
|
+
return true;
|
|
170
|
+
} catch (e) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The session this workspace should be using right now.
|
|
177
|
+
*
|
|
178
|
+
* THE RULE THAT FIXES THE BOOMERANG: resume only when the workspace matches AND
|
|
179
|
+
* the conversation is recent. Anything else starts fresh. A different directory
|
|
180
|
+
* is a different conversation, full stop.
|
|
181
|
+
*
|
|
182
|
+
* @param {string} workspace Usually process.cwd().
|
|
183
|
+
* @returns {object} The session, with its messages loaded.
|
|
184
|
+
*/
|
|
185
|
+
export function resolveSession(workspace) {
|
|
186
|
+
ensureDir();
|
|
187
|
+
migrateLegacyHistory(workspace);
|
|
188
|
+
|
|
189
|
+
const key = workspaceKey(workspace);
|
|
190
|
+
const now = Date.now();
|
|
191
|
+
|
|
192
|
+
const candidate = listSessions()
|
|
193
|
+
.filter(row => row.workspace && workspaceKey(row.workspace) === key)
|
|
194
|
+
.find(row => now - row.updatedAt < RESUME_WINDOW_MS);
|
|
195
|
+
|
|
196
|
+
if (candidate) {
|
|
197
|
+
const session = readSession(candidate.id);
|
|
198
|
+
if (session) {
|
|
199
|
+
setActiveSession(session.id);
|
|
200
|
+
return session;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return createSession(workspace);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Load a specific session by id, making it active. */
|
|
208
|
+
export function switchSession(id) {
|
|
209
|
+
const session = readSession(id);
|
|
210
|
+
if (!session) return null;
|
|
211
|
+
setActiveSession(session.id);
|
|
212
|
+
return session;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Persist a session's messages. Title is set once, from the first user turn. */
|
|
216
|
+
export function saveSessionMessages(id, messages) {
|
|
217
|
+
const session = readSession(id);
|
|
218
|
+
if (!session) return false;
|
|
219
|
+
|
|
220
|
+
session.messages = Array.isArray(messages) ? messages : [];
|
|
221
|
+
session.updatedAt = Date.now();
|
|
222
|
+
|
|
223
|
+
if (!session.title || session.title === 'Untitled') {
|
|
224
|
+
const firstUser = session.messages.find(m => m.role === 'user' && typeof m.content === 'string');
|
|
225
|
+
if (firstUser) session.title = titleFrom(firstUser.content);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return writeSession(session);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function deleteSession(id) {
|
|
232
|
+
try {
|
|
233
|
+
fs.unlinkSync(sessionFile(id));
|
|
234
|
+
if (getActiveSessionId() === id) setActiveSession(null);
|
|
235
|
+
return true;
|
|
236
|
+
} catch (e) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Move the one global history into a session, once.
|
|
243
|
+
*
|
|
244
|
+
* Nothing is thrown away — the conversation that existed becomes a session
|
|
245
|
+
* belonging to the workspace it is first seen from, and the legacy file is
|
|
246
|
+
* renamed rather than deleted so it can be recovered if the migration is wrong.
|
|
247
|
+
*/
|
|
248
|
+
export function migrateLegacyHistory(workspace) {
|
|
249
|
+
try {
|
|
250
|
+
if (!fs.existsSync(LEGACY_HISTORY)) return null;
|
|
251
|
+
|
|
252
|
+
const raw = fs.readFileSync(LEGACY_HISTORY, 'utf8');
|
|
253
|
+
const messages = JSON.parse(raw);
|
|
254
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
255
|
+
fs.renameSync(LEGACY_HISTORY, LEGACY_HISTORY + '.migrated');
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const firstUser = messages.find(m => m.role === 'user' && typeof m.content === 'string');
|
|
260
|
+
const session = createSession(workspace, titleFrom(firstUser ? firstUser.content : 'Imported conversation'));
|
|
261
|
+
session.messages = messages.filter(m => m.role !== 'system');
|
|
262
|
+
session.createdAt = fs.statSync(LEGACY_HISTORY).mtimeMs;
|
|
263
|
+
// NO WORKSPACE, deliberately. The legacy file belonged to every directory
|
|
264
|
+
// at once, so attaching it to whichever one happens to migrate it first
|
|
265
|
+
// would just recreate the boomerang somewhere arbitrary. It stays listed
|
|
266
|
+
// and switchable by name; it is never what `continue` continues.
|
|
267
|
+
session.workspace = '';
|
|
268
|
+
session.workspaceKey = '';
|
|
269
|
+
session.title = session.title === 'Untitled' ? 'Imported conversation' : session.title + ' (imported)';
|
|
270
|
+
writeSession(session);
|
|
271
|
+
|
|
272
|
+
fs.renameSync(LEGACY_HISTORY, LEGACY_HISTORY + '.migrated');
|
|
273
|
+
return session;
|
|
274
|
+
} catch (e) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export { SESSION_DIR, RESUME_WINDOW_MS, MAX_SESSIONS };
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
import { COMPILED_SKILLS } from './compiledSkills.js';
|
|
5
|
+
import { loadUserSkills } from './userSkills.js';
|
|
6
|
+
|
|
7
|
+
// User skills are read from disk, and the disk is slow relative to how often
|
|
8
|
+
// these functions are called (every completion keystroke asks for the
|
|
9
|
+
// catalogue). Cache for a moment so typing stays instant, but keep the window
|
|
10
|
+
// short enough that a skill the agent just wrote shows up without a restart.
|
|
11
|
+
const USER_CACHE_MS = 2000;
|
|
12
|
+
let userCache = { at: 0, skills: [], problems: [] };
|
|
13
|
+
|
|
14
|
+
export function invalidateUserSkills() {
|
|
15
|
+
userCache = { at: 0, skills: [], problems: [] };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function userSkills() {
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
if (now - userCache.at < USER_CACHE_MS) return userCache;
|
|
21
|
+
try {
|
|
22
|
+
const loaded = loadUserSkills();
|
|
23
|
+
userCache = { at: now, skills: loaded.skills, problems: loaded.problems };
|
|
24
|
+
} catch (e) {
|
|
25
|
+
// A broken skills directory must never stop a session from starting.
|
|
26
|
+
userCache = { at: now, skills: [], problems: [`user skills unavailable: ${e.message}`] };
|
|
27
|
+
}
|
|
28
|
+
return userCache;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function userSkillProblems() {
|
|
32
|
+
return userSkills().problems;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// What a workspace looks like from the outside.
|
|
36
|
+
//
|
|
37
|
+
// This used to name four domains, which meant that of the compiled skills only
|
|
38
|
+
// android, web, python and devops could EVER be surfaced — every other one was
|
|
39
|
+
// compiled into the artefact and unreachable. Detection is what decides whether
|
|
40
|
+
// a skill exists as far as the model is concerned.
|
|
41
|
+
//
|
|
42
|
+
// Marker files only; no globbing and no directory walking, so detection stays a
|
|
43
|
+
// handful of stat calls whatever the size of the repository.
|
|
44
|
+
const DOMAIN_INDICATORS = {
|
|
45
|
+
android: ['build.gradle', 'build.gradle.kts', 'app/build.gradle', 'app/build.gradle.kts',
|
|
46
|
+
'app/src/main/AndroidManifest.xml', 'settings.gradle.kts', 'gradlew'],
|
|
47
|
+
mobile: ['pubspec.yaml', 'ios/Podfile', 'Podfile', 'app.json', 'metro.config.js',
|
|
48
|
+
'react-native.config.js', 'expo.json'],
|
|
49
|
+
web: ['next.config.js', 'next.config.mjs', 'next.config.ts', 'nuxt.config.ts',
|
|
50
|
+
'svelte.config.js', 'vue.config.js', 'astro.config.mjs', 'vite.config.js',
|
|
51
|
+
'vite.config.ts', 'angular.json', 'index.html', 'tailwind.config.js'],
|
|
52
|
+
backend: ['package.json', 'go.mod', 'Cargo.toml', 'pom.xml', 'build.sbt',
|
|
53
|
+
'Gemfile', 'composer.json', 'nest-cli.json'],
|
|
54
|
+
python: ['requirements.txt', 'pyproject.toml', 'Pipfile', 'setup.py', 'setup.cfg',
|
|
55
|
+
'uv.lock', 'poetry.lock', 'environment.yml'],
|
|
56
|
+
devops: ['Dockerfile', 'docker-compose.yml', 'docker-compose.yaml', '.github/workflows',
|
|
57
|
+
'.gitlab-ci.yml', 'Jenkinsfile', 'Makefile', '.circleci/config.yml'],
|
|
58
|
+
cloud: ['main.tf', 'terraform.tf', 'serverless.yml', 'template.yaml', 'Pulumi.yaml',
|
|
59
|
+
'cdk.json', 'k8s', 'kubernetes', 'helm', 'skaffold.yaml'],
|
|
60
|
+
database: ['prisma/schema.prisma', 'migrations', 'alembic.ini', 'knexfile.js',
|
|
61
|
+
'schema.sql', 'db/schema.rb', 'drizzle.config.ts', 'flyway.conf'],
|
|
62
|
+
testing: ['jest.config.js', 'jest.config.ts', 'vitest.config.ts', 'playwright.config.ts',
|
|
63
|
+
'cypress.config.js', 'pytest.ini', 'tox.ini', 'tests', 'test', '__tests__'],
|
|
64
|
+
security: ['.github/dependabot.yml', 'SECURITY.md', '.snyk', 'socket.yml', 'socket.json'],
|
|
65
|
+
design: ['tailwind.config.js', 'tailwind.config.ts', 'styles', 'src/styles',
|
|
66
|
+
'theme.js', 'theme.ts', 'tokens.json', 'postcss.config.js'],
|
|
67
|
+
// A 3D/video workspace. Without a rule here the media skills compile into
|
|
68
|
+
// the artefact and are unreachable by domain detection — tool_search still
|
|
69
|
+
// finds them, but the workspace never volunteers them.
|
|
70
|
+
media: ['.blend', 'assets/3d', 'renders', 'blender', 'shots', 'Resolve Projects'],
|
|
71
|
+
git: ['.git', '.gitignore', '.gitattributes'],
|
|
72
|
+
performance: ['lighthouserc.js', 'lighthouserc.json', '.lighthouserc.yml', 'k6.js',
|
|
73
|
+
'benchmark', 'benchmarks'],
|
|
74
|
+
'ai-engineering': ['.env.example', 'prompts', 'evals', 'langchain.json',
|
|
75
|
+
'llm.config.js', 'openai.json'],
|
|
76
|
+
data: ['dbt_project.yml', 'airflow.cfg', 'dags', 'notebooks', '*.ipynb',
|
|
77
|
+
'dvc.yaml', 'great_expectations']
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Domains that apply to every task regardless of what the workspace looks like.
|
|
81
|
+
//
|
|
82
|
+
// Design and security are not properties of a file layout — an agent writing a
|
|
83
|
+
// UI needs the anti-slop direction whether or not a tailwind config exists, and
|
|
84
|
+
// input validation matters whether or not the repository has a SECURITY.md.
|
|
85
|
+
const UNIVERSAL_DOMAINS = ['design', 'security'];
|
|
86
|
+
|
|
87
|
+
// Ceiling on how many skill descriptions may enter the system prompt.
|
|
88
|
+
//
|
|
89
|
+
// With 100 skills, injecting every description of every detected domain would
|
|
90
|
+
// spend thousands of tokens on an index before the task has even been read.
|
|
91
|
+
// This is the progressive-disclosure contract: the model sees names and routing
|
|
92
|
+
// conditions, and pulls a body with fs_read_skill only when one is relevant.
|
|
93
|
+
const MAX_INDEXED_SKILLS = 40;
|
|
94
|
+
|
|
95
|
+
export function detectWorkspaceDomains(workspaceRoot) {
|
|
96
|
+
if (!workspaceRoot || !fs.existsSync(workspaceRoot)) return UNIVERSAL_DOMAINS.slice();
|
|
97
|
+
|
|
98
|
+
const detectedDomains = [];
|
|
99
|
+
|
|
100
|
+
for (const [domain, indicators] of Object.entries(DOMAIN_INDICATORS)) {
|
|
101
|
+
for (const indicator of indicators) {
|
|
102
|
+
// A glob marker means "any file with this extension at the root". Kept to
|
|
103
|
+
// one readdir rather than a walk — a recursive search of a large monorepo
|
|
104
|
+
// on every task start is not worth one extra domain.
|
|
105
|
+
if (indicator.startsWith('*.')) {
|
|
106
|
+
try {
|
|
107
|
+
const extension = indicator.slice(1);
|
|
108
|
+
if (fs.readdirSync(workspaceRoot).some((name) => name.endsWith(extension))) {
|
|
109
|
+
detectedDomains.push(domain);
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
} catch (e) { /* unreadable directory is simply not a match */ }
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (fs.existsSync(path.join(workspaceRoot, indicator))) {
|
|
116
|
+
detectedDomains.push(domain);
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const domain of UNIVERSAL_DOMAINS) {
|
|
123
|
+
if (!detectedDomains.includes(domain)) detectedDomains.push(domain);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return detectedDomains;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export { MAX_INDEXED_SKILLS, UNIVERSAL_DOMAINS };
|
|
130
|
+
|
|
131
|
+
export function ensureSkillRepo(domain) {
|
|
132
|
+
|
|
133
|
+
if (COMPILED_SKILLS[domain]) {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The index the model sees: names and routing conditions, never bodies.
|
|
141
|
+
*
|
|
142
|
+
* Progressive disclosure. 100 skills is roughly 250 KB of markdown; injecting it
|
|
143
|
+
* would consume the context window before the task was read. What goes in is one
|
|
144
|
+
* line per skill — the name and WHEN TO USE IT — and the model pulls the body
|
|
145
|
+
* with fs_read_skill only for the one it needs. This is the same contract
|
|
146
|
+
* claude-code and openclaw use, and it is what makes a large skill library
|
|
147
|
+
* possible at all.
|
|
148
|
+
*
|
|
149
|
+
* Capped at MAX_INDEXED_SKILLS. A workspace matching eight domains would
|
|
150
|
+
* otherwise inject fifty-odd descriptions; the detected domains come first so
|
|
151
|
+
* the cut falls on the least relevant.
|
|
152
|
+
*/
|
|
153
|
+
export function getUniversalSkillsIndex(domains) {
|
|
154
|
+
let indexText = '';
|
|
155
|
+
let totalSkillsCount = 0;
|
|
156
|
+
|
|
157
|
+
// One catalogue, so a user skill is as visible to the agent as a compiled one.
|
|
158
|
+
const catalogue = new Map();
|
|
159
|
+
for (const skill of listCompiledSkills()) {
|
|
160
|
+
if (!catalogue.has(skill.domain)) catalogue.set(skill.domain, []);
|
|
161
|
+
catalogue.get(skill.domain).push(skill);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// A user's own domains are always offered: someone who wrote a `team/` skill
|
|
165
|
+
// wants it considered, and no marker file in the workspace announces it.
|
|
166
|
+
const ownDomains = [...new Set(userSkills().skills.map((skill) => skill.domain))];
|
|
167
|
+
const wanted = [...new Set([...ownDomains, ...domains])];
|
|
168
|
+
|
|
169
|
+
for (const domain of wanted) {
|
|
170
|
+
const domainSkills = catalogue.get(domain);
|
|
171
|
+
if (!domainSkills || domainSkills.length === 0) continue;
|
|
172
|
+
if (totalSkillsCount >= MAX_INDEXED_SKILLS) break;
|
|
173
|
+
|
|
174
|
+
let domainSkillsCount = 0;
|
|
175
|
+
let domainIndexText = `### ${domain.toUpperCase()} SKILLS\n`;
|
|
176
|
+
|
|
177
|
+
for (const skill of domainSkills) {
|
|
178
|
+
if (totalSkillsCount + domainSkillsCount >= MAX_INDEXED_SKILLS) break;
|
|
179
|
+
const mark = skill.source === 'user' ? ' [your own]' : '';
|
|
180
|
+
domainIndexText += `- **${skill.name}**${mark} (ID: ${domain}/${skill.id}): ${skill.description}\n`;
|
|
181
|
+
domainSkillsCount++;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (domainSkillsCount > 0) {
|
|
185
|
+
indexText += domainIndexText + '\n';
|
|
186
|
+
totalSkillsCount += domainSkillsCount;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (totalSkillsCount === 0) return '';
|
|
191
|
+
|
|
192
|
+
let finalOutput = `\n<causyne_universal_skills>\n`;
|
|
193
|
+
finalOutput += `You have access to highly-curated, domain-specific intelligence skills provided as readable local skill data. If a skill is relevant to the user's task, you MUST use the \`fs_read_skill\` tool (passing the ID) to read its full instruction BEFORE proceeding with your plan. NEVER guess syntax if a skill exists for it.\n\n`;
|
|
194
|
+
finalOutput += indexText;
|
|
195
|
+
finalOutput += `</causyne_universal_skills>\n`;
|
|
196
|
+
|
|
197
|
+
return finalOutput;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The compact form of the skills index: what domains exist and how to search
|
|
202
|
+
* them, instead of forty name-and-description lines.
|
|
203
|
+
*
|
|
204
|
+
* WHY: `getUniversalSkillsIndex()` costs ~841 tokens for two domains and ~1,092
|
|
205
|
+
* for web+android, injected into a cached system block on EVERY request of
|
|
206
|
+
* EVERY task — a bare "hi" paid for a catalogue of skills it would never read.
|
|
207
|
+
*
|
|
208
|
+
* WHY IT IS SAFE NOW AND WAS NOT BEFORE: until `tool_search` learned to score
|
|
209
|
+
* the skill catalogue, this index was the ONLY way a model could discover a
|
|
210
|
+
* skill id. Shrinking it would have made 100 compiled skills unreachable —
|
|
211
|
+
* a capability regression wearing an optimisation's clothes. Retrieval replaces
|
|
212
|
+
* a catalogue only once the retrieval exists.
|
|
213
|
+
*
|
|
214
|
+
* @param {Set|Array} domains Workspace domains detected for this task.
|
|
215
|
+
* @returns {string} System-prompt fragment, '' when nothing exists.
|
|
216
|
+
*/
|
|
217
|
+
export function getSkillDomainHeader(domains) {
|
|
218
|
+
const catalogue = new Map();
|
|
219
|
+
for (const skill of listCompiledSkills()) {
|
|
220
|
+
catalogue.set(skill.domain, (catalogue.get(skill.domain) || 0) + 1);
|
|
221
|
+
}
|
|
222
|
+
if (catalogue.size === 0) return '';
|
|
223
|
+
|
|
224
|
+
const total = [...catalogue.values()].reduce((sum, n) => sum + n, 0);
|
|
225
|
+
const relevant = [...new Set(domains || [])].filter(d => catalogue.has(d));
|
|
226
|
+
const all = [...catalogue.keys()].sort();
|
|
227
|
+
|
|
228
|
+
let out = `\n<causyne_universal_skills>\n`;
|
|
229
|
+
out += `${total} curated engineering skills are compiled into you, across: ${all.join(', ')}.\n`;
|
|
230
|
+
if (relevant.length > 0) {
|
|
231
|
+
out += `This workspace looks like: ${relevant.join(', ')}.\n`;
|
|
232
|
+
}
|
|
233
|
+
out += `Call \`tool_search\` with what you are trying to do to find the relevant skill ids, then read one with \`fs_read_skill\`. Do this BEFORE planning anything the skills cover. NEVER guess syntax a skill documents.\n`;
|
|
234
|
+
out += `</causyne_universal_skills>\n`;
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* The whole catalogue as a flat list, for the picker and for `/skills`.
|
|
240
|
+
*
|
|
241
|
+
* One shape for every consumer, so the terminal picker and the text listing can
|
|
242
|
+
* never disagree about what exists.
|
|
243
|
+
*/
|
|
244
|
+
export function listCompiledSkills() {
|
|
245
|
+
const out = [];
|
|
246
|
+
const seen = new Set();
|
|
247
|
+
|
|
248
|
+
// The user's own skills come first and SHADOW a compiled skill of the same
|
|
249
|
+
// id. That is the customisation: someone whose deployment genuinely differs
|
|
250
|
+
// from the shipped procedure writes their own `devops/zero-downtime-deploys`
|
|
251
|
+
// and theirs is the one the agent reads. Shipping a library that cannot be
|
|
252
|
+
// corrected is shipping an opinion.
|
|
253
|
+
for (const skill of userSkills().skills) {
|
|
254
|
+
const key = `${skill.domain}/${skill.id}`;
|
|
255
|
+
seen.add(key);
|
|
256
|
+
out.push({
|
|
257
|
+
domain: skill.domain, id: skill.id, name: skill.name,
|
|
258
|
+
description: skill.description, source: 'user'
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
for (const [domain, skills] of Object.entries(COMPILED_SKILLS)) {
|
|
263
|
+
for (const [id, data] of Object.entries(skills)) {
|
|
264
|
+
if (seen.has(`${domain}/${id}`)) continue;
|
|
265
|
+
out.push({
|
|
266
|
+
domain, id, name: data.name || id,
|
|
267
|
+
description: data.description || '', source: 'native'
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function readCompiledSkill(domain, skillId) {
|
|
275
|
+
// A user skill of the same id wins, for the same reason it does in the
|
|
276
|
+
// listing: the user's version is the corrected one.
|
|
277
|
+
const own = userSkills().skills
|
|
278
|
+
.find((skill) => skill.domain === domain && skill.id === skillId);
|
|
279
|
+
if (own) return own.content;
|
|
280
|
+
|
|
281
|
+
const domainSkills = COMPILED_SKILLS[domain];
|
|
282
|
+
if (!domainSkills) return `Error: Domain ${domain} not found in compiled skills.`;
|
|
283
|
+
|
|
284
|
+
const skillData = domainSkills[skillId];
|
|
285
|
+
if (!skillData) return `Error: Skill ${skillId} not found in domain ${domain}.`;
|
|
286
|
+
|
|
287
|
+
return skillData.content;
|
|
288
|
+
}
|