morpheus-cli 0.9.24 → 0.9.31
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 +97 -2
- package/dist/channels/telegram.js +165 -120
- package/dist/cli/commands/restart.js +12 -0
- package/dist/config/manager.js +34 -2
- package/dist/config/paths.js +2 -0
- package/dist/config/schemas.js +6 -0
- package/dist/runtime/__tests__/gws-sync.test.js +69 -0
- package/dist/runtime/gws-sync.js +102 -0
- package/dist/runtime/hash-utils.js +15 -0
- package/dist/runtime/hot-reload.js +5 -1
- package/dist/runtime/oracle.js +1 -0
- package/dist/runtime/scaffold.js +4 -0
- package/dist/runtime/skills/loader.js +7 -52
- package/dist/runtime/skills/registry.js +5 -1
- package/dist/runtime/skills/schema.js +1 -1
- package/dist/runtime/skills/tool.js +8 -1
- package/dist/runtime/subagents/apoc.js +154 -1
- package/dist/runtime/subagents/devkit-instrument.js +13 -0
- package/dist/types/config.js +3 -0
- package/dist/ui/assets/{AuditDashboard-CfYKdOEt.js → AuditDashboard-tH9QZTl4.js} +1 -1
- package/dist/ui/assets/{Chat-CYev7-CJ.js → Chat-Cd0uYF8g.js} +1 -1
- package/dist/ui/assets/{Chronos-5KR8aZud.js → Chronos-CWwHYdBl.js} +1 -1
- package/dist/ui/assets/{ConfirmationModal-NFwIYI7B.js → ConfirmationModal-CxvFe-We.js} +1 -1
- package/dist/ui/assets/{Dashboard-hsjB56la.js → Dashboard-CNNMxl53.js} +1 -1
- package/dist/ui/assets/{DeleteConfirmationModal-BfV370Vv.js → DeleteConfirmationModal-AqGQSh1X.js} +1 -1
- package/dist/ui/assets/{Documents-BNo2tMfG.js → Documents-BfRYOK88.js} +1 -1
- package/dist/ui/assets/{Logs-1hBpMPZE.js → Logs-DhFo4cio.js} +1 -1
- package/dist/ui/assets/{MCPManager-CvPRHn4C.js → MCPManager-BMhxbhni.js} +1 -1
- package/dist/ui/assets/{ModelPricing-BbwJFdz4.js → ModelPricing-Dvl0R_HR.js} +1 -1
- package/dist/ui/assets/{Notifications-C_MA51Gf.js → Notifications-CawvBid4.js} +1 -1
- package/dist/ui/assets/{SatiMemories-Cd9xn98_.js → SatiMemories-yyVrJGdc.js} +1 -1
- package/dist/ui/assets/{SessionAudit-BTABenGk.js → SessionAudit-joq0ntdJ.js} +1 -1
- package/dist/ui/assets/{Settings-DRVx4ICA.js → Settings-B6SMPn41.js} +7 -7
- package/dist/ui/assets/{Skills-DS9p1-S8.js → Skills-B5yhTHyn.js} +1 -1
- package/dist/ui/assets/{Smiths-CMCZaAF_.js → Smiths-Dug63YED.js} +1 -1
- package/dist/ui/assets/{Tasks-Cvt4sTcs.js → Tasks-D8HPLkg0.js} +1 -1
- package/dist/ui/assets/{TrinityDatabases-qhSUMeCw.js → TrinityDatabases-D0qEKmwJ.js} +1 -1
- package/dist/ui/assets/{UsageStats-Cy9HKYOp.js → UsageStats-CHWALN70.js} +1 -1
- package/dist/ui/assets/{WebhookManager-ByqkTyqs.js → WebhookManager-T2ef90p8.js} +1 -1
- package/dist/ui/assets/{agents-svEaAPka.js → agents-BVnfnJ1X.js} +1 -1
- package/dist/ui/assets/{audit-gxRPR5Jb.js → audit-BErc_ye8.js} +1 -1
- package/dist/ui/assets/{chronos-ZrBE4yA4.js → chronos-CAv__H3B.js} +1 -1
- package/dist/ui/assets/{config-B1i6Xxwk.js → config-CPFW7PTY.js} +1 -1
- package/dist/ui/assets/{index-DyKlGDg1.js → index-BvsF1a9j.js} +2 -2
- package/dist/ui/assets/{mcp-DSddQR1h.js → mcp-BaHwY4DW.js} +1 -1
- package/dist/ui/assets/{skills-DIuMjpPF.js → skills-lbjIRO8d.js} +1 -1
- package/dist/ui/assets/{stats-CxlRAO2g.js → stats-C8KAfpHO.js} +1 -1
- package/dist/ui/assets/{useCurrency-BkHiWfcT.js → useCurrency-Ch0lsvGj.js} +1 -1
- package/dist/ui/index.html +1 -1
- package/dist/ui/sw.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { syncGwsSkills } from '../gws-sync.js';
|
|
5
|
+
import { ConfigManager } from '../../config/manager.js';
|
|
6
|
+
import { calculateMd5 } from '../hash-utils.js';
|
|
7
|
+
import { tmpdir } from 'os';
|
|
8
|
+
vi.mock('../../config/manager.js');
|
|
9
|
+
vi.mock('../display.js', () => ({
|
|
10
|
+
DisplayManager: {
|
|
11
|
+
getInstance: () => ({
|
|
12
|
+
log: vi.fn(),
|
|
13
|
+
}),
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
describe('GwsSync', () => {
|
|
17
|
+
let mockDestDir;
|
|
18
|
+
let mockHashesFile;
|
|
19
|
+
const mockSourceDir = path.join(process.cwd(), 'gws-skills', 'skills');
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
vi.resetAllMocks();
|
|
22
|
+
// Create a truly temporary directory for this test run
|
|
23
|
+
mockDestDir = path.join(tmpdir(), `morpheus-test-skills-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
24
|
+
mockHashesFile = path.join(mockDestDir, '.gws-hashes.json');
|
|
25
|
+
vi.mocked(ConfigManager.getInstance).mockReturnValue({
|
|
26
|
+
getGwsConfig: () => ({ enabled: true }),
|
|
27
|
+
});
|
|
28
|
+
await fs.ensureDir(mockDestDir);
|
|
29
|
+
});
|
|
30
|
+
afterEach(async () => {
|
|
31
|
+
await fs.remove(mockDestDir);
|
|
32
|
+
});
|
|
33
|
+
it('should copy new skills if destination does not exist', async () => {
|
|
34
|
+
if (!(await fs.pathExists(mockSourceDir))) {
|
|
35
|
+
console.warn('Skipping test: gws-skills/skills not found');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
await syncGwsSkills(mockDestDir);
|
|
39
|
+
const skills = await fs.readdir(mockSourceDir);
|
|
40
|
+
if (skills.length > 0) {
|
|
41
|
+
const firstSkill = skills[0];
|
|
42
|
+
expect(await fs.pathExists(path.join(mockDestDir, firstSkill, 'SKILL.md'))).toBe(true);
|
|
43
|
+
const metadata = await fs.readJson(mockHashesFile);
|
|
44
|
+
expect(metadata.skills[firstSkill]).toBeDefined();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
it('should not overwrite customized skills', async () => {
|
|
48
|
+
if (!(await fs.pathExists(mockSourceDir)))
|
|
49
|
+
return;
|
|
50
|
+
const skills = await fs.readdir(mockSourceDir);
|
|
51
|
+
if (skills.length === 0)
|
|
52
|
+
return;
|
|
53
|
+
const skillName = skills[0];
|
|
54
|
+
const destPath = path.join(mockDestDir, skillName, 'SKILL.md');
|
|
55
|
+
// 1. Initial sync to set baseline
|
|
56
|
+
await syncGwsSkills(mockDestDir);
|
|
57
|
+
const originalMetadata = await fs.readJson(mockHashesFile);
|
|
58
|
+
const originalHash = originalMetadata.skills[skillName];
|
|
59
|
+
// 2. User modifies the file
|
|
60
|
+
await fs.writeFile(destPath, 'USER CUSTOMIZATION');
|
|
61
|
+
const customHash = calculateMd5('USER CUSTOMIZATION');
|
|
62
|
+
expect(customHash).not.toBe(originalHash);
|
|
63
|
+
// 3. Sync again
|
|
64
|
+
await syncGwsSkills(mockDestDir);
|
|
65
|
+
// 4. Verify file was preserved
|
|
66
|
+
const content = await fs.readFile(destPath, 'utf-8');
|
|
67
|
+
expect(content).toBe('USER CUSTOMIZATION');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { PATHS } from '../config/paths.js';
|
|
4
|
+
import { ConfigManager } from '../config/manager.js';
|
|
5
|
+
import { calculateFileMd5 } from './hash-utils.js';
|
|
6
|
+
import { DisplayManager } from './display.js';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import { execSync } from 'child_process';
|
|
9
|
+
/**
|
|
10
|
+
* Checks if a binary is available in the system PATH.
|
|
11
|
+
*/
|
|
12
|
+
function isBinaryAvailable(name) {
|
|
13
|
+
try {
|
|
14
|
+
const command = process.platform === 'win32' ? `where ${name}` : `which ${name}`;
|
|
15
|
+
execSync(command, { stdio: 'ignore' });
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Synchronizes built-in Google Workspace skills to the user's skills directory.
|
|
24
|
+
* Uses MD5 hashes to avoid overwriting user customizations.
|
|
25
|
+
*/
|
|
26
|
+
export async function syncGwsSkills(destOverride) {
|
|
27
|
+
const config = ConfigManager.getInstance().getGwsConfig();
|
|
28
|
+
if (config.enabled === false)
|
|
29
|
+
return;
|
|
30
|
+
const display = DisplayManager.getInstance();
|
|
31
|
+
const sourceDir = path.join(process.cwd(), 'gws-skills', 'skills');
|
|
32
|
+
const destDir = destOverride ?? PATHS.skills;
|
|
33
|
+
const hashesFile = path.join(destDir, '.gws-hashes.json');
|
|
34
|
+
// Check if gws binary is available
|
|
35
|
+
if (!isBinaryAvailable('gws')) {
|
|
36
|
+
display.log(`⚠️ Google Workspace CLI (gws) not found in system PATH. GWS skills will not function.`, { source: 'GwsSync', level: 'warning' });
|
|
37
|
+
}
|
|
38
|
+
// Validate Service Account JSON if provided
|
|
39
|
+
if (config.service_account_json) {
|
|
40
|
+
if (!(await fs.pathExists(config.service_account_json))) {
|
|
41
|
+
display.log(`⚠️ Google Workspace Service Account JSON not found at: ${chalk.yellow(config.service_account_json)}. GWS tools may fail to authenticate.`, { source: 'GwsSync', level: 'warning' });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (!(await fs.pathExists(sourceDir))) {
|
|
45
|
+
// Silent skip if source doesn't exist (e.g. in some production environments)
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
let metadata = { skills: {}, last_sync: new Date().toISOString() };
|
|
50
|
+
if (await fs.pathExists(hashesFile)) {
|
|
51
|
+
metadata = await fs.readJson(hashesFile);
|
|
52
|
+
}
|
|
53
|
+
const builtInSkills = await fs.readdir(sourceDir);
|
|
54
|
+
let newCount = 0;
|
|
55
|
+
let updatedCount = 0;
|
|
56
|
+
let skippedCount = 0;
|
|
57
|
+
for (const skillName of builtInSkills) {
|
|
58
|
+
const skillSourcePath = path.join(sourceDir, skillName, 'SKILL.md');
|
|
59
|
+
const skillDestPath = path.join(destDir, skillName, 'SKILL.md');
|
|
60
|
+
if (!(await fs.pathExists(skillSourcePath)))
|
|
61
|
+
continue;
|
|
62
|
+
const sourceHash = await calculateFileMd5(skillSourcePath);
|
|
63
|
+
if (!(await fs.pathExists(skillDestPath))) {
|
|
64
|
+
// New skill
|
|
65
|
+
await fs.ensureDir(path.dirname(skillDestPath));
|
|
66
|
+
await fs.copy(skillSourcePath, skillDestPath);
|
|
67
|
+
metadata.skills[skillName] = sourceHash;
|
|
68
|
+
newCount++;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// Existing skill - check for customization
|
|
72
|
+
const destHash = await calculateFileMd5(skillDestPath);
|
|
73
|
+
const lastKnownHash = metadata.skills[skillName];
|
|
74
|
+
if (destHash === sourceHash) {
|
|
75
|
+
// Already up to date
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (destHash === lastKnownHash) {
|
|
79
|
+
// Unmodified default, update to latest
|
|
80
|
+
await fs.copy(skillSourcePath, skillDestPath);
|
|
81
|
+
metadata.skills[skillName] = sourceHash;
|
|
82
|
+
updatedCount++;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// User modified or unknown state, preserve
|
|
86
|
+
skippedCount++;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
metadata.last_sync = new Date().toISOString();
|
|
91
|
+
await fs.writeJson(hashesFile, metadata, { spaces: 2 });
|
|
92
|
+
if (newCount > 0 || updatedCount > 0) {
|
|
93
|
+
display.log(`🔧 Google Workspace skills initialized: ${chalk.green(newCount)} new, ${chalk.blue(updatedCount)} updated${skippedCount > 0 ? `, ${chalk.yellow(skippedCount)} customized (skipped)` : ''}`, { source: 'GwsSync', level: 'info' });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
display.log(`Failed to sync Google Workspace skills: ${error instanceof Error ? error.message : String(error)}`, {
|
|
98
|
+
source: 'GwsSync',
|
|
99
|
+
level: 'error',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
/**
|
|
4
|
+
* Calculates the MD5 hash of a string.
|
|
5
|
+
*/
|
|
6
|
+
export function calculateMd5(content) {
|
|
7
|
+
return crypto.createHash('md5').update(content).digest('hex');
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Calculates the MD5 hash of a file.
|
|
11
|
+
*/
|
|
12
|
+
export async function calculateFileMd5(filePath) {
|
|
13
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
14
|
+
return calculateMd5(content);
|
|
15
|
+
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { ConfigManager } from '../config/manager.js';
|
|
9
9
|
import { DisplayManager } from './display.js';
|
|
10
10
|
import { SubagentRegistry } from './subagents/registry.js';
|
|
11
|
+
import { SkillRegistry } from './skills/index.js';
|
|
11
12
|
let currentOracle = null;
|
|
12
13
|
/**
|
|
13
14
|
* Register the current Oracle instance for hot-reload.
|
|
@@ -36,7 +37,10 @@ export async function hotReloadConfig() {
|
|
|
36
37
|
// 1. Reload configuration from disk
|
|
37
38
|
await ConfigManager.getInstance().load();
|
|
38
39
|
display.log('Configuration reloaded from disk', { source: 'HotReload', level: 'info' });
|
|
39
|
-
// 2.
|
|
40
|
+
// 2. Reload skills
|
|
41
|
+
await SkillRegistry.getInstance().reload();
|
|
42
|
+
display.log('Skills reloaded from disk', { source: 'HotReload', level: 'info' });
|
|
43
|
+
// 3. Reinitialize Oracle if it exists
|
|
40
44
|
if (currentOracle && typeof currentOracle.reinitialize === 'function') {
|
|
41
45
|
await currentOracle.reinitialize();
|
|
42
46
|
reinitialized.push('Oracle');
|
package/dist/runtime/oracle.js
CHANGED
|
@@ -340,6 +340,7 @@ Always call time_verifier first, then use the resolved date in your tool call or
|
|
|
340
340
|
11. If a delegation is rejected as "not atomic", immediately split into smaller delegations and retry.
|
|
341
341
|
12. When the user message contains @link, @neo, @apoc, or @trinity (case-insensitive), delegate to that specific agent. The mention is an explicit routing directive — respect it even if another agent might also handle the request.
|
|
342
342
|
13. Smiths also have names and could be called by @smithname — respect this as an explicit routing directive as well.
|
|
343
|
+
14. Delegate Google Workspace (GWS) tasks (Sheets, Docs, Calendar, Drive, Gmail) to the Apoc agent using the \`apoc_delegate\` tool.
|
|
343
344
|
|
|
344
345
|
## Delegation quality ##
|
|
345
346
|
- Write delegation input in the same language requested by the user.
|
package/dist/runtime/scaffold.js
CHANGED
|
@@ -6,6 +6,7 @@ import { DEFAULT_MCP_TEMPLATE } from '../types/mcp.js';
|
|
|
6
6
|
import chalk from 'chalk';
|
|
7
7
|
import ora from 'ora';
|
|
8
8
|
import { migrateConfigFile } from './migration.js';
|
|
9
|
+
import { syncGwsSkills } from './gws-sync.js';
|
|
9
10
|
const SKILLS_README = `# Morpheus Skills
|
|
10
11
|
|
|
11
12
|
This folder contains custom skills for Morpheus.
|
|
@@ -72,6 +73,7 @@ export async function scaffold() {
|
|
|
72
73
|
fs.ensureDir(PATHS.commands),
|
|
73
74
|
fs.ensureDir(PATHS.skills),
|
|
74
75
|
fs.ensureDir(PATHS.docs),
|
|
76
|
+
fs.ensureDir(PATHS.gws),
|
|
75
77
|
]);
|
|
76
78
|
// Migrate config.yaml -> zaion.yaml if needed
|
|
77
79
|
await migrateConfigFile();
|
|
@@ -92,6 +94,8 @@ export async function scaffold() {
|
|
|
92
94
|
if (!(await fs.pathExists(skillsReadme))) {
|
|
93
95
|
await fs.writeFile(skillsReadme, SKILLS_README, 'utf-8');
|
|
94
96
|
}
|
|
97
|
+
// Sync Google Workspace skills
|
|
98
|
+
await syncGwsSkills();
|
|
95
99
|
spinner.succeed('Morpheus environment ready at ' + chalk.cyan(PATHS.root));
|
|
96
100
|
}
|
|
97
101
|
catch (error) {
|
|
@@ -12,67 +12,22 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import fs from 'fs';
|
|
14
14
|
import path from 'path';
|
|
15
|
+
import yaml from 'js-yaml';
|
|
15
16
|
import { SkillMetadataSchema } from './schema.js';
|
|
16
17
|
import { DisplayManager } from '../display.js';
|
|
17
18
|
const SKILL_MD = 'SKILL.md';
|
|
18
19
|
const MAX_SKILL_MD_SIZE = 50 * 1024; // 50KB
|
|
19
20
|
const FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
20
21
|
/**
|
|
21
|
-
*
|
|
22
|
-
* Handles basic key: value pairs and arrays
|
|
22
|
+
* Parses YAML frontmatter using js-yaml
|
|
23
23
|
*/
|
|
24
|
-
function parseFrontmatter(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
let currentKey = null;
|
|
28
|
-
let currentArray = null;
|
|
29
|
-
for (const line of lines) {
|
|
30
|
-
const trimmed = line.trim();
|
|
31
|
-
if (!trimmed || trimmed.startsWith('#'))
|
|
32
|
-
continue;
|
|
33
|
-
// Check for array item (indented with -)
|
|
34
|
-
if (trimmed.startsWith('- ') && currentKey && currentArray !== null) {
|
|
35
|
-
currentArray.push(trimmed.slice(2).trim().replace(/^["']|["']$/g, ''));
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
// Check for key: value
|
|
39
|
-
const colonIndex = line.indexOf(':');
|
|
40
|
-
if (colonIndex > 0) {
|
|
41
|
-
// Save previous array if exists
|
|
42
|
-
if (currentKey && currentArray !== null && currentArray.length > 0) {
|
|
43
|
-
result[currentKey] = currentArray;
|
|
44
|
-
}
|
|
45
|
-
currentArray = null;
|
|
46
|
-
const key = line.slice(0, colonIndex).trim();
|
|
47
|
-
const value = line.slice(colonIndex + 1).trim();
|
|
48
|
-
currentKey = key;
|
|
49
|
-
if (value === '') {
|
|
50
|
-
// Could be start of array
|
|
51
|
-
currentArray = [];
|
|
52
|
-
}
|
|
53
|
-
else if (value === 'true') {
|
|
54
|
-
result[key] = true;
|
|
55
|
-
}
|
|
56
|
-
else if (value === 'false') {
|
|
57
|
-
result[key] = false;
|
|
58
|
-
}
|
|
59
|
-
else if (/^\d+$/.test(value)) {
|
|
60
|
-
result[key] = parseInt(value, 10);
|
|
61
|
-
}
|
|
62
|
-
else if (/^\d+\.\d+$/.test(value)) {
|
|
63
|
-
result[key] = parseFloat(value);
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
// Remove quotes if present
|
|
67
|
-
result[key] = value.replace(/^["']|["']$/g, '');
|
|
68
|
-
}
|
|
69
|
-
}
|
|
24
|
+
function parseFrontmatter(yamlContent) {
|
|
25
|
+
try {
|
|
26
|
+
return yaml.load(yamlContent);
|
|
70
27
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
result[currentKey] = currentArray;
|
|
28
|
+
catch (err) {
|
|
29
|
+
throw new Error(`YAML parse error: ${err instanceof Error ? err.message : String(err)}`);
|
|
74
30
|
}
|
|
75
|
-
return result;
|
|
76
31
|
}
|
|
77
32
|
export class SkillLoader {
|
|
78
33
|
skillsDir;
|
|
@@ -114,7 +114,11 @@ export class SkillRegistry {
|
|
|
114
114
|
|
|
115
115
|
${skillList}
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
MANDATORY SKILL WORKFLOW:
|
|
118
|
+
1. If a user request matches any skill above, you MUST call \`load_skill\` BEFORE calling any delegation tool (apoc_delegate, neo_delegate, etc.).
|
|
119
|
+
2. After loading the skill, include the skill content in the \`context\` parameter when delegating to a subagent.
|
|
120
|
+
3. NEVER delegate a GWS/Google Workspace task without first loading the relevant skill — the subagent needs the exact CLI syntax from the skill instructions.
|
|
121
|
+
4. Example: User asks "create a Google Calendar event" → call \`load_skill("gws-calendar-insert")\` → then call \`apoc_delegate\` with the skill content in \`context\`.`;
|
|
118
122
|
}
|
|
119
123
|
/**
|
|
120
124
|
* Get skill names for tool description
|
|
@@ -55,7 +55,14 @@ function buildLoadSkillDescription() {
|
|
|
55
55
|
const skillList = enabled.length > 0
|
|
56
56
|
? enabled.map(s => `- ${s.name}: ${s.description}`).join('\n')
|
|
57
57
|
: '(no skills available)';
|
|
58
|
-
return `Load a skill's
|
|
58
|
+
return `MANDATORY: Load a skill's expert instructions BEFORE delegating a task that matches an available skill.
|
|
59
|
+
|
|
60
|
+
CRITICAL: For Google Workspace (GWS) tasks, you MUST:
|
|
61
|
+
1. Call load_skill with the matching gws-* skill name FIRST
|
|
62
|
+
2. Then delegate to apoc_delegate, passing the loaded skill content in the "context" parameter
|
|
63
|
+
3. NEVER delegate GWS tasks without loading the skill — Apoc needs the exact CLI syntax
|
|
64
|
+
|
|
65
|
+
After loading, the skill content tells you (and the subagent) the exact commands, resources, and parameters to use.
|
|
59
66
|
|
|
60
67
|
Available skills:
|
|
61
68
|
${skillList}`;
|
|
@@ -8,6 +8,8 @@ import { instrumentDevKitTools } from "./devkit-instrument.js";
|
|
|
8
8
|
import { extractRawUsage, persistAgentMessage, buildAgentResult, emitToolAuditEvents } from "./utils.js";
|
|
9
9
|
import { buildDelegationTool } from "../tools/delegation-utils.js";
|
|
10
10
|
import { SubagentRegistry } from "./registry.js";
|
|
11
|
+
import { USER_HOME } from "../../config/paths.js";
|
|
12
|
+
import { SkillRegistry } from "../skills/index.js";
|
|
11
13
|
/**
|
|
12
14
|
* Apoc is a subagent of Oracle specialized in devtools operations.
|
|
13
15
|
* It receives delegated tasks from Oracle and executes them using DevKit tools
|
|
@@ -34,6 +36,35 @@ export class Apoc {
|
|
|
34
36
|
static setSessionId(sessionId) {
|
|
35
37
|
Apoc.currentSessionId = sessionId;
|
|
36
38
|
}
|
|
39
|
+
/** Update tool description with available skills */
|
|
40
|
+
static async refreshDelegateCatalog() {
|
|
41
|
+
if (Apoc._delegateTool) {
|
|
42
|
+
const skills = SkillRegistry.getInstance().getEnabled();
|
|
43
|
+
const gwsSkills = skills.filter(s => s.name.startsWith('gws-') || s.name.startsWith('recipe-'));
|
|
44
|
+
let description = `Delegate a devtools task to Apoc, the specialized development subagent.
|
|
45
|
+
|
|
46
|
+
This tool enqueues a background task and returns an acknowledgement with task id.
|
|
47
|
+
Do not expect final execution output in the same response.
|
|
48
|
+
Each task must contain a single atomic action with a clear expected result.
|
|
49
|
+
|
|
50
|
+
Use this tool when the user asks for ANY of the following:
|
|
51
|
+
- File operations: read, write, create, delete files or directories
|
|
52
|
+
- Shell commands: run scripts, execute commands, check output
|
|
53
|
+
- Git: status, log, diff, commit, push, pull, clone, branch
|
|
54
|
+
- Package management: npm install/update/audit, yarn, package.json inspection
|
|
55
|
+
- Process management: list processes, kill processes, check ports
|
|
56
|
+
- Network: ping hosts, curl URLs, DNS lookups
|
|
57
|
+
- System info: environment variables, OS info, disk space, memory
|
|
58
|
+
- Internet search: search DuckDuckGo and verify facts by reading at least 3 sources via browser_navigate before reporting results.
|
|
59
|
+
- Browser automation: navigate websites (JS/SPA), inspect DOM, click elements, fill forms. Apoc will ask for missing user input (e.g. credentials, form fields) before proceeding.
|
|
60
|
+
- Google Workspace (GWS) operations: manage Sheets, Docs, Calendar, Drive, Gmail using the \`gws\` CLI. Apoc will ensure proper authentication is set for each command and report any errors encountered.`;
|
|
61
|
+
if (gwsSkills.length > 0) {
|
|
62
|
+
description += '\n\nAvailable Google Workspace (GWS) and Recipe capabilities:\n' +
|
|
63
|
+
gwsSkills.map(s => `- ${s.name}: ${s.description}`).join('\n');
|
|
64
|
+
}
|
|
65
|
+
Apoc._delegateTool.description = description;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
37
68
|
static getInstance(config) {
|
|
38
69
|
if (!Apoc.instance) {
|
|
39
70
|
Apoc.instance = new Apoc(config);
|
|
@@ -45,9 +76,10 @@ export class Apoc {
|
|
|
45
76
|
bgClass: 'bg-amber-50 dark:bg-amber-900/10',
|
|
46
77
|
badgeClass: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300',
|
|
47
78
|
instance: Apoc.instance,
|
|
48
|
-
hasDynamicDescription:
|
|
79
|
+
hasDynamicDescription: true,
|
|
49
80
|
isMultiInstance: false,
|
|
50
81
|
setSessionId: (id) => Apoc.setSessionId(id),
|
|
82
|
+
refreshCatalog: () => Apoc.refreshDelegateCatalog(),
|
|
51
83
|
});
|
|
52
84
|
}
|
|
53
85
|
return Apoc.instance;
|
|
@@ -84,6 +116,120 @@ export class Apoc {
|
|
|
84
116
|
throw new ProviderError(apocConfig.provider, err, "Apoc subagent initialization failed");
|
|
85
117
|
}
|
|
86
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Auto-resolve relevant skills for a task by matching keywords to skill names.
|
|
121
|
+
* Returns the concatenated skill content to inject into Apoc's system prompt.
|
|
122
|
+
*/
|
|
123
|
+
resolveSkillsForTask(task, context) {
|
|
124
|
+
const registry = SkillRegistry.getInstance();
|
|
125
|
+
const enabled = registry.getEnabled();
|
|
126
|
+
if (enabled.length === 0)
|
|
127
|
+
return '';
|
|
128
|
+
const combined = `${task} ${context || ''}`.toLowerCase();
|
|
129
|
+
// Check if skill content was already provided in the context (Oracle loaded it)
|
|
130
|
+
if (context && context.includes('Loaded skill:'))
|
|
131
|
+
return '';
|
|
132
|
+
// GWS keyword → skill name mapping
|
|
133
|
+
const gwsKeywordMap = {
|
|
134
|
+
'tasks': ['gws-tasks'],
|
|
135
|
+
'task': ['gws-tasks'],
|
|
136
|
+
'google tasks': ['gws-tasks'],
|
|
137
|
+
'calendar': ['gws-calendar', 'gws-calendar-insert', 'gws-calendar-agenda'],
|
|
138
|
+
'evento': ['gws-calendar', 'gws-calendar-insert'],
|
|
139
|
+
'event': ['gws-calendar', 'gws-calendar-insert'],
|
|
140
|
+
'agenda': ['gws-calendar-agenda', 'gws-calendar'],
|
|
141
|
+
'gmail': ['gws-gmail', 'gws-gmail-send'],
|
|
142
|
+
'email': ['gws-gmail', 'gws-gmail-send'],
|
|
143
|
+
'e-mail': ['gws-gmail', 'gws-gmail-send'],
|
|
144
|
+
'enviar email': ['gws-gmail-send'],
|
|
145
|
+
'send email': ['gws-gmail-send'],
|
|
146
|
+
'forward': ['gws-gmail-forward'],
|
|
147
|
+
'encaminhar': ['gws-gmail-forward'],
|
|
148
|
+
'reply': ['gws-gmail-reply'],
|
|
149
|
+
'responder email': ['gws-gmail-reply'],
|
|
150
|
+
'drive': ['gws-drive', 'gws-drive-upload'],
|
|
151
|
+
'upload': ['gws-drive-upload'],
|
|
152
|
+
'docs': ['gws-docs', 'gws-docs-write'],
|
|
153
|
+
'documento': ['gws-docs', 'gws-docs-write'],
|
|
154
|
+
'document': ['gws-docs', 'gws-docs-write'],
|
|
155
|
+
'sheets': ['gws-sheets'],
|
|
156
|
+
'planilha': ['gws-sheets'],
|
|
157
|
+
'spreadsheet': ['gws-sheets'],
|
|
158
|
+
'meet': ['gws-meet'],
|
|
159
|
+
'reunião': ['gws-meet'],
|
|
160
|
+
'meeting': ['gws-meet'],
|
|
161
|
+
'forms': ['gws-forms'],
|
|
162
|
+
'formulário': ['gws-forms'],
|
|
163
|
+
'keep': ['gws-keep'],
|
|
164
|
+
'nota': ['gws-keep'],
|
|
165
|
+
'note': ['gws-keep'],
|
|
166
|
+
'chat': ['gws-chat', 'gws-chat-send'],
|
|
167
|
+
'classroom': ['gws-classroom'],
|
|
168
|
+
};
|
|
169
|
+
const matchedSkills = new Set();
|
|
170
|
+
// Check if task involves GWS at all
|
|
171
|
+
const isGwsTask = combined.includes('gws') || combined.includes('google') ||
|
|
172
|
+
Object.keys(gwsKeywordMap).some(kw => combined.includes(kw));
|
|
173
|
+
if (!isGwsTask)
|
|
174
|
+
return '';
|
|
175
|
+
// Find matching skills by keyword
|
|
176
|
+
for (const [keyword, skillNames] of Object.entries(gwsKeywordMap)) {
|
|
177
|
+
if (combined.includes(keyword)) {
|
|
178
|
+
for (const name of skillNames) {
|
|
179
|
+
matchedSkills.add(name);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// If GWS task but no specific match, try to match by skill name fragments
|
|
184
|
+
if (matchedSkills.size === 0 && isGwsTask) {
|
|
185
|
+
for (const skill of enabled) {
|
|
186
|
+
if (skill.name.startsWith('gws-')) {
|
|
187
|
+
const service = skill.name.replace('gws-', '').replace(/-/g, ' ');
|
|
188
|
+
if (combined.includes(service)) {
|
|
189
|
+
matchedSkills.add(skill.name);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// Also check recipe skills
|
|
195
|
+
for (const skill of enabled) {
|
|
196
|
+
if (skill.name.startsWith('recipe-')) {
|
|
197
|
+
const recipe = skill.name.replace('recipe-', '').replace(/-/g, ' ');
|
|
198
|
+
if (combined.includes(recipe)) {
|
|
199
|
+
matchedSkills.add(skill.name);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (matchedSkills.size === 0)
|
|
204
|
+
return '';
|
|
205
|
+
// Load skill content (limit to 3 most relevant)
|
|
206
|
+
const skillContents = [];
|
|
207
|
+
let count = 0;
|
|
208
|
+
for (const name of matchedSkills) {
|
|
209
|
+
if (count >= 3)
|
|
210
|
+
break;
|
|
211
|
+
const skill = registry.get(name);
|
|
212
|
+
if (skill?.enabled && skill.content) {
|
|
213
|
+
skillContents.push(`━━━ SKILL: ${skill.name} ━━━\n${skill.content}`);
|
|
214
|
+
count++;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (skillContents.length === 0)
|
|
218
|
+
return '';
|
|
219
|
+
this.display.log(`Auto-injected ${skillContents.length} skill(s) for task: ${[...matchedSkills].slice(0, 3).join(', ')}`, {
|
|
220
|
+
source: 'Apoc',
|
|
221
|
+
level: 'info',
|
|
222
|
+
});
|
|
223
|
+
return `\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
224
|
+
LOADED SKILLS — FOLLOW THESE INSTRUCTIONS
|
|
225
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
226
|
+
|
|
227
|
+
The following skill instructions tell you EXACTLY how to execute this task.
|
|
228
|
+
Use \`gws schema <resource>.<method>\` to discover required params before calling any method.
|
|
229
|
+
Do NOT guess command syntax — follow the skill instructions below.
|
|
230
|
+
|
|
231
|
+
${skillContents.join('\n\n')}`;
|
|
232
|
+
}
|
|
87
233
|
/**
|
|
88
234
|
* Execute a devtools task delegated by Oracle.
|
|
89
235
|
* @param task Natural language task description
|
|
@@ -99,6 +245,8 @@ export class Apoc {
|
|
|
99
245
|
source: "Apoc",
|
|
100
246
|
});
|
|
101
247
|
const personality = this.config.apoc?.personality || 'pragmatic_dev';
|
|
248
|
+
// Auto-resolve and inject relevant skill content for the task
|
|
249
|
+
const skillSection = this.resolveSkillsForTask(task, context);
|
|
102
250
|
const systemMessage = new SystemMessage(`
|
|
103
251
|
You are Apoc, ${personality === 'pragmatic_dev' ? 'a pragmatic and methodical developer' : personality}, a high-reliability execution and verification subagent inside the Morpheus system.
|
|
104
252
|
|
|
@@ -117,6 +265,10 @@ CORE PRINCIPLES
|
|
|
117
265
|
• Prefer authoritative sources over secondary commentary.
|
|
118
266
|
• Prefer verification over assumption.
|
|
119
267
|
• Explicitly measure and report confidence.
|
|
268
|
+
• You have the capability to execute \`gws\` CLI commands for Google Workspace operations (Sheets, Calendar, Drive, etc.).
|
|
269
|
+
• **CRITICAL GWS AUTH**: Every time you run a \`gws\` command, you MUST ensure \`GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE\` is set to the absolute path of your credentials file: \`${USER_HOME}/.morpheus/gws/credentials.json\` (or the equivalent absolute path on the current OS). If not already set in your environment, prepend the command with the export (e.g., \`export GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE=... && gws ...\` on Linux/macOS or \`$env:GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE='...'; gws ...\` on Windows).
|
|
270
|
+
• If the \`gws\` command fails, report the error detail. If the tool is not installed, inform the user clearly.
|
|
271
|
+
• **IMPORTANT**: When skill instructions are loaded below, follow them EXACTLY. Do NOT guess CLI syntax — use \`gws schema\` to discover params first.${skillSection}
|
|
120
272
|
|
|
121
273
|
If reliable evidence cannot be obtained:
|
|
122
274
|
State clearly:
|
|
@@ -312,6 +464,7 @@ Use this tool when the user asks for ANY of the following:
|
|
|
312
464
|
- System info: environment variables, OS info, disk space, memory
|
|
313
465
|
- Internet search: search DuckDuckGo and verify facts by reading at least 3 sources via browser_navigate before reporting results.
|
|
314
466
|
- Browser automation: navigate websites (JS/SPA), inspect DOM, click elements, fill forms. Apoc will ask for missing user input (e.g. credentials, form fields) before proceeding.
|
|
467
|
+
- Google Workspace (GWS) operations: manage Sheets, Docs, Calendar, Drive, Gmail using the \`gws\` CLI. Apoc will ensure proper authentication is set for each command and report any errors encountered.
|
|
315
468
|
|
|
316
469
|
Provide a clear natural language task description. Optionally provide context
|
|
317
470
|
from the current conversation to help Apoc understand the broader goal.`,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AuditRepository } from '../audit/repository.js';
|
|
2
2
|
import { DisplayManager } from '../display.js';
|
|
3
|
+
import { ConfigManager } from '../../config/manager.js';
|
|
3
4
|
const display = DisplayManager.getInstance();
|
|
4
5
|
/**
|
|
5
6
|
* Wraps a StructuredTool to record audit events on each invocation.
|
|
@@ -12,6 +13,18 @@ function instrumentTool(tool, getSessionId, getAgent) {
|
|
|
12
13
|
const startMs = Date.now();
|
|
13
14
|
const sessionId = getSessionId() ?? 'unknown';
|
|
14
15
|
const agent = getAgent();
|
|
16
|
+
// Inject GWS credentials if it's a shell tool and command starts with 'gws'
|
|
17
|
+
if ((tool.name === 'execShell' || tool.name === 'execCommand') && input?.command?.trim().startsWith('gws')) {
|
|
18
|
+
const gwsConfig = ConfigManager.getInstance().getGwsConfig();
|
|
19
|
+
if (gwsConfig.service_account_json) {
|
|
20
|
+
input.env = {
|
|
21
|
+
...(input.env || {}),
|
|
22
|
+
GOOGLE_APPLICATION_CREDENTIALS: gwsConfig.service_account_json,
|
|
23
|
+
GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE: gwsConfig.service_account_json,
|
|
24
|
+
};
|
|
25
|
+
display.log(`Injected GWS credentials into environment for tool ${tool.name}`, { source: 'DevKitInstrumentation', level: 'debug' });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
15
28
|
display.startActivity(agent, `Executing tool: ${tool.name}`);
|
|
16
29
|
try {
|
|
17
30
|
const result = await original(input, runManager);
|