@jamaynor/hal-config 1.0.2 → 1.1.0
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/CLAUDE.md +2 -1
- package/lib/config.js +257 -18
- package/lib/types.d.ts +218 -0
- package/package.json +1 -1
- package/test/test.js +28 -11
- package/publish.ps1 +0 -30
package/CLAUDE.md
CHANGED
|
@@ -52,7 +52,8 @@ halConfig.halSkillConfig.setSetting(skillNameOrScope, key, value) // merge singl
|
|
|
52
52
|
|
|
53
53
|
// Registration (for init wizards to write back)
|
|
54
54
|
halConfig.register(name, data) // Merge into skills[name].runtime
|
|
55
|
-
halConfig.
|
|
55
|
+
halConfig.upsertCommunicationAccount(account) // Upsert one communication account by label
|
|
56
|
+
halConfig.deleteCommunicationAccount(label) // Delete one communication account by label
|
|
56
57
|
halConfig.writeSkillConfig(name, data) // Merge into skills[name]
|
|
57
58
|
halConfig.writeSharedSettings(settings) // Write top-level keys (hal-timezone, etc.)
|
|
58
59
|
```
|
package/lib/config.js
CHANGED
|
@@ -1,11 +1,37 @@
|
|
|
1
1
|
// =============================================================================
|
|
2
|
-
// hal-
|
|
2
|
+
// hal-config/lib/config
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// SRP (Single Responsibility)
|
|
5
|
+
// - Load + cache `hal-system-config.json` and provide a single, stable API for
|
|
6
|
+
// HAL/OpenClaw skills to read shared settings (vaults, accounts, timezone,
|
|
7
|
+
// work hours), resolve workspaces, and locate per-skill config/log directories.
|
|
7
8
|
//
|
|
8
|
-
//
|
|
9
|
+
// Public Interface
|
|
10
|
+
// config
|
|
11
|
+
// ├── Loading / cache
|
|
12
|
+
// │ ├── load(configDir?)
|
|
13
|
+
// │ ├── reload()
|
|
14
|
+
// │ └── configDir()
|
|
15
|
+
// ├── Runtime context (primary read API)
|
|
16
|
+
// │ ├── getRuntimeContext(opts?) -> HalRuntimeContext
|
|
17
|
+
// │ │ opts.callingAgentRuntimePath?: string|null
|
|
18
|
+
// │ │ - If null/unresolved, returns only `general` (no `callingAgent`)
|
|
19
|
+
// │ └── HalRuntimeContext
|
|
20
|
+
// │ ├── general
|
|
21
|
+
// │ └── callingAgent? (optional)
|
|
22
|
+
// │ └── skills[*].agentHasAccess (resolved from homeAgent + caller)
|
|
23
|
+
// ├── Installation checks
|
|
24
|
+
// │ ├── isInstalled(skillName)
|
|
25
|
+
// │ └── isInstalledAsync(skillName)
|
|
26
|
+
// ├── Per-skill config I/O (skill-owned files; separate from hal-system-config.json)
|
|
27
|
+
// │ ├── getSkillConfig(skillName, workspace)
|
|
28
|
+
// │ └── saveSkillConfig(skillName, workspace, data)
|
|
29
|
+
// └── Init-wizard writes (mutate hal-system-config.json)
|
|
30
|
+
// ├── register(name, runtimeData)
|
|
31
|
+
// ├── upsertCommunicationAccount(account)
|
|
32
|
+
// ├── deleteCommunicationAccount(label)
|
|
33
|
+
// ├── writeSkillConfig(name, data)
|
|
34
|
+
// └── writeSharedSettings(settings)
|
|
9
35
|
// =============================================================================
|
|
10
36
|
|
|
11
37
|
import fs from 'node:fs';
|
|
@@ -474,6 +500,190 @@ export function isInstalledAsync(name) {
|
|
|
474
500
|
return _isBinaryOnPathAsync(bins[0]);
|
|
475
501
|
}
|
|
476
502
|
|
|
503
|
+
function _canWriteDir(dirPath) {
|
|
504
|
+
try {
|
|
505
|
+
if (!dirPath) return false;
|
|
506
|
+
fs.accessSync(dirPath, fs.constants.W_OK);
|
|
507
|
+
return true;
|
|
508
|
+
} catch {
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function _deriveAgentIdFromRuntimePath(runtimePath) {
|
|
514
|
+
const normalized = path.resolve(String(runtimePath || '').trim());
|
|
515
|
+
if (!normalized) return null;
|
|
516
|
+
const id = path.basename(normalized);
|
|
517
|
+
return id || null;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function _vaultDirectoryName(vaultPath) {
|
|
521
|
+
const p = String(vaultPath || '').trim();
|
|
522
|
+
if (!p) return '';
|
|
523
|
+
const normalized = p.replace(/[\\/]+$/, '');
|
|
524
|
+
const useWin = normalized.includes('\\') || /^[a-zA-Z]:\\/.test(normalized);
|
|
525
|
+
return useWin ? path.win32.basename(normalized) : path.posix.basename(normalized);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function _resolveMasterVault(vaultEntries) {
|
|
529
|
+
const masters = vaultEntries.filter(v => v.role === 'master');
|
|
530
|
+
if (masters.length === 0) return null;
|
|
531
|
+
if (masters.length > 1) {
|
|
532
|
+
throw new Error('ERROR: Multiple master vaults configured in hal-obsidian-vaults.');
|
|
533
|
+
}
|
|
534
|
+
const m = masters[0];
|
|
535
|
+
return {
|
|
536
|
+
name: m.name,
|
|
537
|
+
label: m.label,
|
|
538
|
+
absolutePath: m.absolutePath,
|
|
539
|
+
directoryName: m.directoryName,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Build a normalized runtime context for HAL/OpenClaw consumers.
|
|
545
|
+
*
|
|
546
|
+
* @param {{ callingAgentRuntimePath?: string|null }} [opts]
|
|
547
|
+
* @returns {{ general: object, callingAgent?: object }}
|
|
548
|
+
*/
|
|
549
|
+
export function getRuntimeContext(opts) {
|
|
550
|
+
_ensure();
|
|
551
|
+
const o = opts || {};
|
|
552
|
+
|
|
553
|
+
const openClawRoot = '/data/openclaw';
|
|
554
|
+
const openClawAgentRoot = '/data/agents';
|
|
555
|
+
const openClawConfigFile = path.join(openClawRoot, 'openclaw.json');
|
|
556
|
+
const openClawSharedAgentWorkspace = openclawSharedWorkspaceRoot();
|
|
557
|
+
const halRoot = halSystemConfigDir();
|
|
558
|
+
const halSystemConfigFile = halSystemConfigFilePath();
|
|
559
|
+
|
|
560
|
+
const rawVaults = vaults();
|
|
561
|
+
const vaultEntries = rawVaults.map(v => {
|
|
562
|
+
const name = String(v?.name || '').trim();
|
|
563
|
+
const label = String(v?.label || name).trim();
|
|
564
|
+
const absolutePath = String(v?.path || '').trim();
|
|
565
|
+
const role = String(v?.role || v?.vaultType || 'standard').trim().toLowerCase() === 'master'
|
|
566
|
+
? 'master'
|
|
567
|
+
: 'standard';
|
|
568
|
+
const folderNames = vaultFolders(name);
|
|
569
|
+
return {
|
|
570
|
+
name,
|
|
571
|
+
label,
|
|
572
|
+
role,
|
|
573
|
+
absolutePath,
|
|
574
|
+
directoryName: _vaultDirectoryName(absolutePath),
|
|
575
|
+
knownAreas: {
|
|
576
|
+
projectsPath: _joinVaultSubpath(absolutePath, folderNames.projectsFolder),
|
|
577
|
+
dailyNotesPath: _joinVaultSubpath(absolutePath, folderNames.dailyNotesFolder),
|
|
578
|
+
emailPath: _joinVaultSubpath(absolutePath, folderNames.emailFolder),
|
|
579
|
+
meetingsPath: _joinVaultSubpath(absolutePath, folderNames.meetingsFolder),
|
|
580
|
+
peoplePath: _joinVaultSubpath(absolutePath, folderNames.peopleFolder),
|
|
581
|
+
},
|
|
582
|
+
};
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
const skillMap = _cache.skills || {};
|
|
586
|
+
const skills = {};
|
|
587
|
+
for (const [skillName, skillData] of Object.entries(skillMap)) {
|
|
588
|
+
const bins = (() => {
|
|
589
|
+
const bin = skillData?.install?.binary;
|
|
590
|
+
if (!bin) return [];
|
|
591
|
+
return Array.isArray(bin) ? bin : [bin];
|
|
592
|
+
})();
|
|
593
|
+
skills[skillName] = {
|
|
594
|
+
enabled: skillData?.enabled !== false,
|
|
595
|
+
homeAgent: skillData?.homeAgent || null,
|
|
596
|
+
binaries: bins,
|
|
597
|
+
configPath: skillConfigPath(skillName),
|
|
598
|
+
configDir: skillConfigDir(skillName),
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const general = {
|
|
603
|
+
openClawPaths: {
|
|
604
|
+
openClawRoot,
|
|
605
|
+
openClawAgentRoot,
|
|
606
|
+
openClawConfigFile,
|
|
607
|
+
openClawSharedAgentWorkspace,
|
|
608
|
+
},
|
|
609
|
+
halPaths: {
|
|
610
|
+
halRoot,
|
|
611
|
+
halSystemConfigDir: halRoot,
|
|
612
|
+
halSystemConfigFile,
|
|
613
|
+
},
|
|
614
|
+
masterTemplates: {
|
|
615
|
+
masterUserPath: path.join(openClawSharedAgentWorkspace, 'USER.md'),
|
|
616
|
+
masterSafetyPath: path.join(openClawSharedAgentWorkspace, 'SAFETY.md'),
|
|
617
|
+
},
|
|
618
|
+
vaults: vaultEntries,
|
|
619
|
+
masterVault: _resolveMasterVault(vaultEntries),
|
|
620
|
+
communicationAccounts: _cache['hal-communication-accounts'] || [],
|
|
621
|
+
settings: {
|
|
622
|
+
timezone: _cache['hal-timezone'] || null,
|
|
623
|
+
workHours: _cache['hal-work-hours'] || null,
|
|
624
|
+
},
|
|
625
|
+
skills,
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
const runtimePathRaw = (o.callingAgentRuntimePath ?? process.env.HAL_AGENT_WORKSPACE ?? '').toString().trim();
|
|
629
|
+
if (!runtimePathRaw) return { general };
|
|
630
|
+
|
|
631
|
+
const runtimePath = path.resolve(runtimePathRaw);
|
|
632
|
+
const agentId = _deriveAgentIdFromRuntimePath(runtimePath);
|
|
633
|
+
|
|
634
|
+
const skillPathsBySkill = {};
|
|
635
|
+
const accessBySkill = {};
|
|
636
|
+
const agentSkillState = {};
|
|
637
|
+
for (const [skillName, skillData] of Object.entries(skills)) {
|
|
638
|
+
const sharedConfigDir = halSkillConfigDir(skillName);
|
|
639
|
+
const sharedLogDir = halSkillLogDir(skillName);
|
|
640
|
+
const agentWorkingDir = path.join(runtimePath, 'hal', skillName);
|
|
641
|
+
skillPathsBySkill[skillName] = {
|
|
642
|
+
skillName,
|
|
643
|
+
sharedConfigDir,
|
|
644
|
+
sharedLogDir,
|
|
645
|
+
agentWorkingDir,
|
|
646
|
+
agentStateDir: path.join(agentWorkingDir, 'state'),
|
|
647
|
+
agentCacheDir: path.join(agentWorkingDir, 'cache'),
|
|
648
|
+
agentTmpDir: path.join(agentWorkingDir, 'tmp'),
|
|
649
|
+
agentSweepDir: path.join(agentWorkingDir, 'sweep'),
|
|
650
|
+
};
|
|
651
|
+
const hasAccess = !skillData.homeAgent || (agentId && skillData.homeAgent === agentId);
|
|
652
|
+
accessBySkill[skillName] = !!hasAccess;
|
|
653
|
+
agentSkillState[skillName] = { agentHasAccess: !!hasAccess };
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const callingAgent = {
|
|
657
|
+
id: agentId,
|
|
658
|
+
workspacePath: runtimePath,
|
|
659
|
+
identityFiles: {
|
|
660
|
+
soul: path.join(runtimePath, 'SOUL.md'),
|
|
661
|
+
agents: path.join(runtimePath, 'AGENTS.md'),
|
|
662
|
+
identity: path.join(runtimePath, 'IDENTITY.md'),
|
|
663
|
+
heartbeat: path.join(runtimePath, 'HEARTBEAT.md'),
|
|
664
|
+
tools: path.join(runtimePath, 'TOOLS.md'),
|
|
665
|
+
user: path.join(runtimePath, 'USER.md'),
|
|
666
|
+
safety: path.join(runtimePath, 'SAFETY.md'),
|
|
667
|
+
},
|
|
668
|
+
stores: {
|
|
669
|
+
memoryDir: path.join(runtimePath, 'memory'),
|
|
670
|
+
knowledgeDir: path.join(runtimePath, 'knowledge'),
|
|
671
|
+
skillsDir: path.join(runtimePath, 'skills'),
|
|
672
|
+
},
|
|
673
|
+
skillPaths: {
|
|
674
|
+
bySkill: skillPathsBySkill,
|
|
675
|
+
},
|
|
676
|
+
skills: agentSkillState,
|
|
677
|
+
capabilities: {
|
|
678
|
+
canWriteAgentWorkspace: _canWriteDir(runtimePath),
|
|
679
|
+
canWriteSharedWorkspace: _canWriteDir(openClawSharedAgentWorkspace),
|
|
680
|
+
isHomeAgentForSkill: accessBySkill,
|
|
681
|
+
},
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
return { general, callingAgent };
|
|
685
|
+
}
|
|
686
|
+
|
|
477
687
|
// ---------------------------------------------------------------------------
|
|
478
688
|
// Binary helpers
|
|
479
689
|
// ---------------------------------------------------------------------------
|
|
@@ -521,25 +731,51 @@ export function register(name, runtimeData) {
|
|
|
521
731
|
}
|
|
522
732
|
|
|
523
733
|
/**
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
* @param {
|
|
734
|
+
* Upsert a single communication account by label.
|
|
735
|
+
*
|
|
736
|
+
* @param {import('./types').HalCommunicationAccountEntry} account
|
|
527
737
|
*/
|
|
528
|
-
export function
|
|
738
|
+
export function upsertCommunicationAccount(account) {
|
|
529
739
|
_ensure();
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
} else {
|
|
536
|
-
existing.push(acc);
|
|
537
|
-
}
|
|
740
|
+
if (!account || typeof account !== 'object') {
|
|
741
|
+
throw new Error('ERROR: upsertCommunicationAccount requires an account object.');
|
|
742
|
+
}
|
|
743
|
+
if (!account.label || typeof account.label !== 'string') {
|
|
744
|
+
throw new Error('ERROR: upsertCommunicationAccount requires account.label (string).');
|
|
538
745
|
}
|
|
746
|
+
if (!account.provider || typeof account.provider !== 'string') {
|
|
747
|
+
throw new Error('ERROR: upsertCommunicationAccount requires account.provider (string).');
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const existing = _cache['hal-communication-accounts'] || [];
|
|
751
|
+
const idx = existing.findIndex(e => e.label === account.label);
|
|
752
|
+
if (idx >= 0) existing[idx] = account;
|
|
753
|
+
else existing.push(account);
|
|
754
|
+
|
|
539
755
|
_cache['hal-communication-accounts'] = existing;
|
|
540
756
|
_flush();
|
|
541
757
|
}
|
|
542
758
|
|
|
759
|
+
/**
|
|
760
|
+
* Delete a communication account by label.
|
|
761
|
+
*
|
|
762
|
+
* @param {string} label
|
|
763
|
+
* @returns {boolean} true when an entry was removed
|
|
764
|
+
*/
|
|
765
|
+
export function deleteCommunicationAccount(label) {
|
|
766
|
+
_ensure();
|
|
767
|
+
if (!label || typeof label !== 'string') {
|
|
768
|
+
throw new Error('ERROR: deleteCommunicationAccount requires label (string).');
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const existing = _cache['hal-communication-accounts'] || [];
|
|
772
|
+
const before = existing.length;
|
|
773
|
+
_cache['hal-communication-accounts'] = existing.filter(e => e?.label !== label);
|
|
774
|
+
const removed = _cache['hal-communication-accounts'].length < before;
|
|
775
|
+
if (removed) _flush();
|
|
776
|
+
return removed;
|
|
777
|
+
}
|
|
778
|
+
|
|
543
779
|
/**
|
|
544
780
|
* Merge key/value pairs into a skill's config block (not runtime — top level
|
|
545
781
|
* of the skill entry). Used by init wizards to store skill-specific settings.
|
|
@@ -623,7 +859,7 @@ export function resolveWorkspace(opts) {
|
|
|
623
859
|
* @param {string} workspace
|
|
624
860
|
* @returns {object}
|
|
625
861
|
*/
|
|
626
|
-
export function
|
|
862
|
+
export function getSkillConfig(skillName, workspace) {
|
|
627
863
|
const filePath = path.join(workspace, 'hal', 'config', `${skillName}.json`);
|
|
628
864
|
try {
|
|
629
865
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
@@ -633,6 +869,9 @@ export function loadSkillConfig(skillName, workspace) {
|
|
|
633
869
|
}
|
|
634
870
|
}
|
|
635
871
|
|
|
872
|
+
// Backward compatibility alias; prefer getSkillConfig().
|
|
873
|
+
export const loadSkillConfig = getSkillConfig;
|
|
874
|
+
|
|
636
875
|
/**
|
|
637
876
|
* Deep-merge source into target. Plain objects are merged recursively.
|
|
638
877
|
* Arrays are replaced atomically. Non-object values overwrite.
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* hal-config/lib/types
|
|
4
|
+
*
|
|
5
|
+
* SRP (Single Responsibility)
|
|
6
|
+
* - Define the shared TypeScript contracts for HAL/OpenClaw runtime context:
|
|
7
|
+
* global platform state plus calling-agent state used by skills at runtime.
|
|
8
|
+
*
|
|
9
|
+
* Interface Tree
|
|
10
|
+
HalRuntimeContext
|
|
11
|
+
├── general: HalGeneralState
|
|
12
|
+
│ ├── openClawPaths
|
|
13
|
+
│ │ ├── openClawRoot
|
|
14
|
+
│ │ ├── openClawAgentRoot
|
|
15
|
+
│ │ ├── openClawConfigFile
|
|
16
|
+
│ │ └── openClawSharedAgentWorkspace
|
|
17
|
+
│ ├── halPaths
|
|
18
|
+
│ │ ├── halRoot
|
|
19
|
+
│ │ ├── halSystemConfigDir
|
|
20
|
+
│ │ └── halSystemConfigFile
|
|
21
|
+
│ ├── masterTemplates
|
|
22
|
+
│ │ ├── masterUserPath (e.g. {openClawSharedAgentWorkspace}/USER.md)
|
|
23
|
+
│ │ └── masterSafetyPath (e.g. {openClawSharedAgentWorkspace}/SAFETY.md)
|
|
24
|
+
│ ├── vaults: HalVaultEntry[]
|
|
25
|
+
│ │ └── HalVaultEntry
|
|
26
|
+
│ │ ├── name
|
|
27
|
+
│ │ ├── label
|
|
28
|
+
│ │ ├── role (master|standard)
|
|
29
|
+
│ │ ├── absolutePath
|
|
30
|
+
│ │ ├── directoryName
|
|
31
|
+
│ │ └── knownAreas
|
|
32
|
+
│ │ ├── projectsPath
|
|
33
|
+
│ │ ├── dailyNotesPath
|
|
34
|
+
│ │ ├── emailPath
|
|
35
|
+
│ │ ├── meetingsPath
|
|
36
|
+
│ │ └── peoplePath
|
|
37
|
+
│ ├── masterVault
|
|
38
|
+
│ │ ├── name
|
|
39
|
+
│ │ ├── label
|
|
40
|
+
│ │ ├── absolutePath
|
|
41
|
+
│ │ └── directoryName
|
|
42
|
+
│ ├── communicationAccounts: HalCommunicationAccountEntry[]
|
|
43
|
+
│ │ └── HalCommunicationAccountEntry
|
|
44
|
+
│ │ ├── label
|
|
45
|
+
│ │ ├── provider
|
|
46
|
+
│ │ ├── email?
|
|
47
|
+
│ │ └── scopes?
|
|
48
|
+
│ ├── settings
|
|
49
|
+
│ │ ├── timezone
|
|
50
|
+
│ │ └── workHours
|
|
51
|
+
│ │ ├── start
|
|
52
|
+
│ │ └── end
|
|
53
|
+
│ └── skills: Record<string, HalSkillRuntimeEntry>
|
|
54
|
+
│ └── HalSkillRuntimeEntry
|
|
55
|
+
│ ├── enabled
|
|
56
|
+
│ ├── homeAgent
|
|
57
|
+
│ ├── binaries[]
|
|
58
|
+
│ ├── configPath
|
|
59
|
+
│ └── configDir
|
|
60
|
+
└── callingAgent: HalCallingAgentState
|
|
61
|
+
├── id
|
|
62
|
+
├── workspacePath
|
|
63
|
+
├── identityFiles
|
|
64
|
+
│ ├── soul
|
|
65
|
+
│ ├── agents
|
|
66
|
+
│ ├── identity
|
|
67
|
+
│ ├── heartbeat
|
|
68
|
+
│ ├── tools
|
|
69
|
+
│ ├── user
|
|
70
|
+
│ └── safety
|
|
71
|
+
├── stores
|
|
72
|
+
│ ├── memoryDir
|
|
73
|
+
│ ├── knowledgeDir
|
|
74
|
+
│ └── skillsDir
|
|
75
|
+
├── skillPaths
|
|
76
|
+
│ └── bySkill: Record<string, HalSkillPathSet>
|
|
77
|
+
│ └── HalSkillPathSet
|
|
78
|
+
│ ├── skillName
|
|
79
|
+
│ ├── sharedConfigDir
|
|
80
|
+
│ ├── sharedLogDir
|
|
81
|
+
│ ├── agentWorkingDir
|
|
82
|
+
│ ├── agentStateDir
|
|
83
|
+
│ ├── agentCacheDir
|
|
84
|
+
│ ├── agentTmpDir
|
|
85
|
+
│ └── agentSweepDir
|
|
86
|
+
└── capabilities
|
|
87
|
+
├── canWriteAgentWorkspace
|
|
88
|
+
├── canWriteSharedWorkspace
|
|
89
|
+
└── isHomeAgentForSkill: Record<string, boolean>
|
|
90
|
+
* =============================================================================
|
|
91
|
+
*/
|
|
92
|
+
export type VaultRole = 'master' | 'standard';
|
|
93
|
+
|
|
94
|
+
export interface HalVaultAreas {
|
|
95
|
+
projectsFolder: string;
|
|
96
|
+
dailyNotesFolder: string;
|
|
97
|
+
emailFolder: string;
|
|
98
|
+
meetingsFolder: string;
|
|
99
|
+
peopleFolder: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface HalVaultResolvedAreas {
|
|
103
|
+
projectsPath: string;
|
|
104
|
+
dailyNotesPath: string;
|
|
105
|
+
emailPath: string;
|
|
106
|
+
meetingsPath: string;
|
|
107
|
+
peoplePath: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface HalVaultEntry {
|
|
111
|
+
name: string;
|
|
112
|
+
label: string;
|
|
113
|
+
role: VaultRole;
|
|
114
|
+
absolutePath: string;
|
|
115
|
+
directoryName: string;
|
|
116
|
+
knownAreas: HalVaultResolvedAreas;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface HalMasterVaultRef {
|
|
120
|
+
name: string;
|
|
121
|
+
label: string;
|
|
122
|
+
absolutePath: string;
|
|
123
|
+
directoryName: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface HalCommunicationAccountEntry {
|
|
127
|
+
label: string;
|
|
128
|
+
provider: string;
|
|
129
|
+
email?: string;
|
|
130
|
+
scopes?: string[];
|
|
131
|
+
[key: string]: unknown;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface HalSkillRuntimeEntry {
|
|
135
|
+
enabled: boolean;
|
|
136
|
+
homeAgent: string | null;
|
|
137
|
+
binaries: string[];
|
|
138
|
+
configPath: string;
|
|
139
|
+
configDir: string;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface HalSkillPathSet {
|
|
143
|
+
skillName: string;
|
|
144
|
+
sharedConfigDir: string;
|
|
145
|
+
sharedLogDir: string;
|
|
146
|
+
agentWorkingDir: string;
|
|
147
|
+
agentStateDir: string;
|
|
148
|
+
agentCacheDir: string;
|
|
149
|
+
agentTmpDir: string;
|
|
150
|
+
agentSweepDir: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface HalGeneralState {
|
|
154
|
+
openClawPaths: {
|
|
155
|
+
openClawRoot: string;
|
|
156
|
+
openClawAgentRoot: string;
|
|
157
|
+
openClawConfigFile: string;
|
|
158
|
+
openClawSharedAgentWorkspace: string;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
halPaths: {
|
|
162
|
+
halRoot: string;
|
|
163
|
+
halSystemConfigDir: string;
|
|
164
|
+
halSystemConfigFile: string;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
masterTemplates: {
|
|
168
|
+
masterUserPath: string;
|
|
169
|
+
masterSafetyPath: string;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
vaults: HalVaultEntry[];
|
|
173
|
+
masterVault: HalMasterVaultRef | null;
|
|
174
|
+
communicationAccounts: HalCommunicationAccountEntry[];
|
|
175
|
+
|
|
176
|
+
settings: {
|
|
177
|
+
timezone: string | null;
|
|
178
|
+
workHours: { start: string; end: string } | null;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
skills: Record<string, HalSkillRuntimeEntry>;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface HalCallingAgentState {
|
|
185
|
+
id: string | null;
|
|
186
|
+
workspacePath: string | null;
|
|
187
|
+
|
|
188
|
+
identityFiles: {
|
|
189
|
+
soul: string | null;
|
|
190
|
+
agents: string | null;
|
|
191
|
+
identity: string | null;
|
|
192
|
+
heartbeat: string | null;
|
|
193
|
+
tools: string | null;
|
|
194
|
+
user: string | null;
|
|
195
|
+
safety: string | null;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
stores: {
|
|
199
|
+
memoryDir: string | null;
|
|
200
|
+
knowledgeDir: string | null;
|
|
201
|
+
skillsDir: string | null;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
skillPaths: {
|
|
205
|
+
bySkill: Record<string, HalSkillPathSet>;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
capabilities: {
|
|
209
|
+
canWriteAgentWorkspace: boolean;
|
|
210
|
+
canWriteSharedWorkspace: boolean;
|
|
211
|
+
isHomeAgentForSkill: Record<string, boolean>;
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface HalRuntimeContext {
|
|
216
|
+
general: HalGeneralState;
|
|
217
|
+
callingAgent: HalCallingAgentState;
|
|
218
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jamaynor/hal-config",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Shared configuration loader for HAL OpenClaw skills — reads system-config.json, provides skill discovery, and runtime registration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
package/test/test.js
CHANGED
|
@@ -495,12 +495,12 @@ describe('hal-shared-config', () => {
|
|
|
495
495
|
});
|
|
496
496
|
});
|
|
497
497
|
|
|
498
|
-
describe('
|
|
499
|
-
it('appends new
|
|
498
|
+
describe('upsertCommunicationAccount()', () => {
|
|
499
|
+
it('appends new account', () => {
|
|
500
500
|
hal.load(tmpDir);
|
|
501
|
-
hal.
|
|
502
|
-
{ label: 'imap@example.com', provider: 'imap', email: 'imap@example.com' }
|
|
503
|
-
|
|
501
|
+
hal.upsertCommunicationAccount(
|
|
502
|
+
{ label: 'imap@example.com', provider: 'imap', email: 'imap@example.com' }
|
|
503
|
+
);
|
|
504
504
|
|
|
505
505
|
const ondisk = readConfig(tmpDir);
|
|
506
506
|
assert.equal(ondisk['hal-communication-accounts'].length, 3);
|
|
@@ -508,10 +508,10 @@ describe('hal-shared-config', () => {
|
|
|
508
508
|
|
|
509
509
|
it('replaces existing account by label', () => {
|
|
510
510
|
hal.load(tmpDir);
|
|
511
|
-
hal.
|
|
511
|
+
hal.upsertCommunicationAccount(
|
|
512
512
|
{ label: 'me@gmail.com', provider: 'google', email: 'me@gmail.com',
|
|
513
|
-
scopes: ['email'] }
|
|
514
|
-
|
|
513
|
+
scopes: ['email'] }
|
|
514
|
+
);
|
|
515
515
|
|
|
516
516
|
const ondisk = readConfig(tmpDir);
|
|
517
517
|
assert.equal(ondisk['hal-communication-accounts'].length, 2);
|
|
@@ -522,9 +522,9 @@ describe('hal-shared-config', () => {
|
|
|
522
522
|
|
|
523
523
|
it('does not remove unmentioned accounts', () => {
|
|
524
524
|
hal.load(tmpDir);
|
|
525
|
-
hal.
|
|
526
|
-
{ label: 'new@example.com', provider: 'imap', email: 'new@example.com' }
|
|
527
|
-
|
|
525
|
+
hal.upsertCommunicationAccount(
|
|
526
|
+
{ label: 'new@example.com', provider: 'imap', email: 'new@example.com' }
|
|
527
|
+
);
|
|
528
528
|
|
|
529
529
|
const ondisk = readConfig(tmpDir);
|
|
530
530
|
const labels = ondisk['hal-communication-accounts'].map(a => a.label);
|
|
@@ -534,6 +534,23 @@ describe('hal-shared-config', () => {
|
|
|
534
534
|
});
|
|
535
535
|
});
|
|
536
536
|
|
|
537
|
+
describe('deleteCommunicationAccount()', () => {
|
|
538
|
+
it('deletes an existing account by label', () => {
|
|
539
|
+
hal.load(tmpDir);
|
|
540
|
+
const removed = hal.deleteCommunicationAccount('me@gmail.com');
|
|
541
|
+
assert.equal(removed, true);
|
|
542
|
+
const ondisk = readConfig(tmpDir);
|
|
543
|
+
const labels = ondisk['hal-communication-accounts'].map(a => a.label);
|
|
544
|
+
assert.ok(!labels.includes('me@gmail.com'));
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it('returns false when label does not exist', () => {
|
|
548
|
+
hal.load(tmpDir);
|
|
549
|
+
const removed = hal.deleteCommunicationAccount('missing@example.com');
|
|
550
|
+
assert.equal(removed, false);
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
|
|
537
554
|
describe('writeSkillConfig()', () => {
|
|
538
555
|
it('merges into skill block', () => {
|
|
539
556
|
hal.load(tmpDir);
|
package/publish.ps1
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
$ErrorActionPreference = 'Stop'
|
|
2
|
-
|
|
3
|
-
Write-Host "Publishing @jamaynor/hal-config from this directory..."
|
|
4
|
-
|
|
5
|
-
# Prompt for token without echoing it to the console.
|
|
6
|
-
$secure = Read-Host -Prompt "Paste npm automation token (input hidden)" -AsSecureString
|
|
7
|
-
$token = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
|
|
8
|
-
[Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure)
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
if (-not $token -or -not $token.Trim()) {
|
|
12
|
-
throw "No token provided."
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
try {
|
|
16
|
-
$env:NPM_TOKEN = $token.Trim()
|
|
17
|
-
|
|
18
|
-
Write-Host ""
|
|
19
|
-
Write-Host "npm whoami:"
|
|
20
|
-
npm whoami
|
|
21
|
-
|
|
22
|
-
Write-Host ""
|
|
23
|
-
Write-Host "npm publish:"
|
|
24
|
-
npm publish
|
|
25
|
-
} finally {
|
|
26
|
-
Remove-Item Env:NPM_TOKEN -ErrorAction SilentlyContinue
|
|
27
|
-
# Reduce lifetime of plaintext token in memory.
|
|
28
|
-
$token = $null
|
|
29
|
-
}
|
|
30
|
-
|