create-walle 0.9.3 → 0.9.4
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 +2 -1
- package/package.json +1 -1
- package/template/claude-task-manager/db.js +5 -1
- package/template/claude-task-manager/public/css/walle.css +317 -0
- package/template/claude-task-manager/public/index.html +404 -101
- package/template/claude-task-manager/public/js/walle.js +1256 -86
- package/template/claude-task-manager/server.js +189 -14
- package/template/docs/site/api/README.md +146 -0
- package/template/docs/site/skills/README.md +99 -5
- package/template/package.json +1 -1
- package/template/wall-e/agent.js +54 -0
- package/template/wall-e/api-walle.js +452 -3
- package/template/wall-e/brain.js +45 -1
- package/template/wall-e/channels/telegram-channel.js +96 -0
- package/template/wall-e/chat.js +61 -2
- package/template/wall-e/coding-context.js +252 -0
- package/template/wall-e/coding-orchestrator.js +625 -0
- package/template/wall-e/coding-review.js +189 -0
- package/template/wall-e/core-tasks.js +12 -3
- package/template/wall-e/deploy.sh +4 -4
- package/template/wall-e/fly.toml +2 -2
- package/template/wall-e/package.json +4 -1
- package/template/wall-e/skills/_bundled/coding-agent/SKILL.md +17 -0
- package/template/wall-e/skills/_bundled/coding-agent/run.js +142 -0
- package/template/wall-e/skills/_bundled/email-sync/SKILL.md +12 -7
- package/template/wall-e/skills/_bundled/email-sync/mail-reader.jxa +76 -46
- package/template/wall-e/skills/_bundled/email-sync/run.js +42 -2
- package/template/wall-e/skills/_bundled/glean-team-sync/SKILL.md +57 -0
- package/template/wall-e/skills/_bundled/glean-team-sync/run.js +254 -0
- package/template/wall-e/skills/_bundled/slack-mentions/SKILL.md +1 -1
- package/template/wall-e/skills/_bundled/slack-mentions/run.js +268 -121
- package/template/wall-e/skills/_templates/data-fetcher.md +27 -0
- package/template/wall-e/skills/_templates/manual-action.md +19 -0
- package/template/wall-e/skills/_templates/periodic-checker.md +29 -0
- package/template/wall-e/skills/_templates/script-runner.md +21 -0
- package/template/wall-e/skills/claude-code-reader.js +16 -4
- package/template/wall-e/skills/skill-executor.js +23 -1
- package/template/wall-e/skills/skill-validator.js +73 -0
- package/template/wall-e/tests/brain.test.js +3 -3
- package/template/wall-e/tests/coding-agent-integration.test.js +240 -0
- package/template/wall-e/tests/coding-context.test.js +212 -0
- package/template/wall-e/tests/coding-orchestrator.test.js +303 -0
- package/template/wall-e/tests/coding-review.test.js +141 -0
- package/template/claude-task-manager/package-lock.json +0 -1607
- package/template/claude-task-manager/tests/test-ai-search.js +0 -61
- package/template/claude-task-manager/tests/test-editor-ux.js +0 -76
- package/template/claude-task-manager/tests/test-editor-ux2.js +0 -51
- package/template/claude-task-manager/tests/test-features-v2.js +0 -127
- package/template/claude-task-manager/tests/test-insights-cached.js +0 -78
- package/template/claude-task-manager/tests/test-insights.js +0 -124
- package/template/claude-task-manager/tests/test-permissions-v2.js +0 -127
- package/template/claude-task-manager/tests/test-permissions.js +0 -122
- package/template/claude-task-manager/tests/test-pin.js +0 -51
- package/template/claude-task-manager/tests/test-prompts.js +0 -164
- package/template/claude-task-manager/tests/test-recent-sessions.js +0 -96
- package/template/claude-task-manager/tests/test-review.js +0 -104
- package/template/claude-task-manager/tests/test-send-dropdown.js +0 -76
- package/template/claude-task-manager/tests/test-send-final.js +0 -30
- package/template/claude-task-manager/tests/test-send-fixes.js +0 -76
- package/template/claude-task-manager/tests/test-send-integration.js +0 -107
- package/template/claude-task-manager/tests/test-send-visual.js +0 -34
- package/template/claude-task-manager/tests/test-session-create.js +0 -147
- package/template/claude-task-manager/tests/test-sidebar-ux.js +0 -83
- package/template/claude-task-manager/tests/test-url-hash.js +0 -68
- package/template/claude-task-manager/tests/test-ux-crop.js +0 -34
- package/template/claude-task-manager/tests/test-ux-review.js +0 -130
- package/template/claude-task-manager/tests/test-zoom-card.js +0 -76
- package/template/claude-task-manager/tests/test-zoom.js +0 -92
- package/template/claude-task-manager/tests/test-zoom2.js +0 -67
- package/template/docs/openclaw-vs-walle-comparison.md +0 -103
- package/template/docs/ux-improvement-plan.md +0 -84
- package/template/wall-e/docs/specs/2026-04-01-publish-plan.md +0 -112
- package/template/wall-e/docs/specs/SKILL-FORMAT.md +0 -326
- package/template/wall-e/package-lock.json +0 -533
- package/template/wall-e/skills/_bundled/slack-mentions/.watermark.json +0 -4
|
@@ -25,9 +25,11 @@ const JXA_SCRIPT = path.join(SKILL_DIR, 'mail-reader.jxa');
|
|
|
25
25
|
const args = process.argv.slice(2);
|
|
26
26
|
let daysBack = 3;
|
|
27
27
|
let syncInbox = false;
|
|
28
|
+
let incremental = false;
|
|
28
29
|
for (let i = 0; i < args.length; i++) {
|
|
29
30
|
if (args[i] === '--days-back' && args[i + 1]) daysBack = parseInt(args[i + 1], 10);
|
|
30
31
|
if (args[i] === '--sync-inbox') syncInbox = true;
|
|
32
|
+
if (args[i] === '--incremental') incremental = true;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
// Task-level config overrides (passed via WALL_E_SKILL_CONFIG env var)
|
|
@@ -36,8 +38,34 @@ try {
|
|
|
36
38
|
? JSON.parse(process.env.WALL_E_SKILL_CONFIG) : {};
|
|
37
39
|
if (envConfig.days_back != null) daysBack = envConfig.days_back;
|
|
38
40
|
if (envConfig.sync_inbox != null) syncInbox = !!envConfig.sync_inbox;
|
|
41
|
+
if (envConfig.incremental != null) incremental = !!envConfig.incremental;
|
|
39
42
|
} catch { /* ignore parse errors */ }
|
|
40
43
|
|
|
44
|
+
// Incremental mode: compute days_back from the latest email already in the brain
|
|
45
|
+
if (incremental) {
|
|
46
|
+
try {
|
|
47
|
+
const brainPath = path.resolve(SKILL_DIR, '..', '..', '..', 'brain.js');
|
|
48
|
+
const brain = require(brainPath);
|
|
49
|
+
brain.initDb();
|
|
50
|
+
const row = brain.getDb().prepare(
|
|
51
|
+
"SELECT max(timestamp) as ts FROM memories WHERE source = 'email'"
|
|
52
|
+
).get();
|
|
53
|
+
brain.closeDb();
|
|
54
|
+
if (row?.ts) {
|
|
55
|
+
const latestDate = new Date(row.ts);
|
|
56
|
+
const now = new Date();
|
|
57
|
+
// Go back 1 extra day for overlap/safety margin
|
|
58
|
+
const computedDays = Math.ceil((now - latestDate) / (24 * 60 * 60 * 1000)) + 1;
|
|
59
|
+
console.error(`[email-sync] Incremental: latest email is ${row.ts}, fetching ${computedDays}d back`);
|
|
60
|
+
daysBack = computedDays;
|
|
61
|
+
} else {
|
|
62
|
+
console.error(`[email-sync] Incremental: no existing emails, falling back to ${daysBack}d`);
|
|
63
|
+
}
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error(`[email-sync] Incremental check failed: ${err.message}, using days_back=${daysBack}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
41
69
|
// ── Step 1: Read emails via JXA ──────────────────────────────────────
|
|
42
70
|
|
|
43
71
|
function readEmails() {
|
|
@@ -191,8 +219,20 @@ async function main() {
|
|
|
191
219
|
return;
|
|
192
220
|
} catch (err) {
|
|
193
221
|
const errMsg = (err.stderr || err.message || '').toString();
|
|
194
|
-
const
|
|
195
|
-
|
|
222
|
+
const isTimeout = errMsg.includes('-1712') || errMsg.includes('timed out');
|
|
223
|
+
const isPermDenied = !isTimeout && (errMsg.includes('not allowed') || errMsg.includes('assistive access')
|
|
224
|
+
|| errMsg.includes('-1743') || errMsg.includes('-1744'));
|
|
225
|
+
|
|
226
|
+
if (isTimeout) {
|
|
227
|
+
console.error(`[email-sync] Mail app timed out (attempt ${attempt + 1}/${MAX_RETRIES + 1}). Mail may be busy, not running, or the query is too large.`);
|
|
228
|
+
if (attempt < MAX_RETRIES) {
|
|
229
|
+
console.error('[email-sync] Retrying in 10s — make sure Mail.app is open and responsive...');
|
|
230
|
+
await sleep(10_000);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
console.error('[email-sync] Still timing out. Try: 1) Open Mail.app and wait for it to finish syncing, 2) Run with a smaller days_back value.');
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
196
236
|
|
|
197
237
|
if (isPermDenied && attempt < MAX_RETRIES) {
|
|
198
238
|
console.error(`[email-sync] Mail access denied (attempt ${attempt + 1}/${MAX_RETRIES + 1}).`);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: glean-team-sync
|
|
3
|
+
description: >
|
|
4
|
+
Sync team structure from Glean people directory into WALL-E's brain.
|
|
5
|
+
Reads the owner's profile, their manager, peers, and direct reports,
|
|
6
|
+
then recursively maps each person's contact info, location, and role.
|
|
7
|
+
version: 1.0.0
|
|
8
|
+
author: wall-e
|
|
9
|
+
execution: script
|
|
10
|
+
entry: run.js
|
|
11
|
+
trigger:
|
|
12
|
+
type: manual
|
|
13
|
+
config:
|
|
14
|
+
owner_name:
|
|
15
|
+
type: string
|
|
16
|
+
default: ""
|
|
17
|
+
description: "Name of the person whose team to sync (defaults to WALLE_OWNER_NAME)"
|
|
18
|
+
depth:
|
|
19
|
+
type: number
|
|
20
|
+
default: 2
|
|
21
|
+
description: "How many levels of org tree to traverse (1=direct team, 2=skip-level reports)"
|
|
22
|
+
tags: [glean, team, people, directory, org, contacts]
|
|
23
|
+
permissions:
|
|
24
|
+
- glean:read
|
|
25
|
+
- brain:write
|
|
26
|
+
---
|
|
27
|
+
# Glean Team Sync
|
|
28
|
+
|
|
29
|
+
## What This Skill Does
|
|
30
|
+
|
|
31
|
+
Syncs team structure from Glean's people directory into WALL-E's brain.
|
|
32
|
+
Starting from the owner, it discovers the org tree (manager, peers, direct
|
|
33
|
+
reports) and stores each person's profile as a memory.
|
|
34
|
+
|
|
35
|
+
## How It Works
|
|
36
|
+
|
|
37
|
+
1. Search Glean for the owner by name via `employee_search` MCP tool
|
|
38
|
+
2. Parse the profile: name, title, department, location, email, manager, reports
|
|
39
|
+
3. Recursively search each team member up to configured depth
|
|
40
|
+
4. Dedup by `source_id` = `glean:person:{email}` — skip unchanged, update modified
|
|
41
|
+
5. Store as memories with `source: 'glean'`, `memory_type: 'team_member'`
|
|
42
|
+
|
|
43
|
+
## Memory Format
|
|
44
|
+
|
|
45
|
+
Each person is stored as a memory:
|
|
46
|
+
- **source**: `glean`
|
|
47
|
+
- **source_id**: `glean:person:{email}`
|
|
48
|
+
- **source_channel**: department or team name
|
|
49
|
+
- **memory_type**: `team_member`
|
|
50
|
+
- **subject**: person's full name
|
|
51
|
+
- **content**: human-readable profile with title, location, contact, org relationship
|
|
52
|
+
- **metadata**: JSON with structured fields (email, title, department, location, manager, reports, relationship)
|
|
53
|
+
- **importance**: 0.5 (team context)
|
|
54
|
+
|
|
55
|
+
## Output
|
|
56
|
+
|
|
57
|
+
Returns: `{ synced, updated, skipped, errors, people: [...names] }`
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
/**
|
|
4
|
+
* glean-team-sync — reads team structure from Glean people directory
|
|
5
|
+
* and stores each person's profile in WALL-E's brain.
|
|
6
|
+
*
|
|
7
|
+
* Uses the Glean MCP `employee_search` tool to find people, then
|
|
8
|
+
* recursively traverses the org tree.
|
|
9
|
+
*
|
|
10
|
+
* Usage: node run.js [--owner "Name"] [--depth N]
|
|
11
|
+
*/
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
const SKILL_DIR = __dirname;
|
|
15
|
+
const PAUSE_MS = 600;
|
|
16
|
+
|
|
17
|
+
// Parse CLI args
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
let ownerName = process.env.WALLE_OWNER_NAME || '';
|
|
20
|
+
let maxDepth = 2;
|
|
21
|
+
for (let i = 0; i < args.length; i++) {
|
|
22
|
+
if (args[i] === '--owner' && args[i + 1]) ownerName = args[i + 1];
|
|
23
|
+
if (args[i] === '--depth' && args[i + 1]) maxDepth = parseInt(args[i + 1], 10);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Task-level config overrides
|
|
27
|
+
try {
|
|
28
|
+
const envConfig = process.env.WALL_E_SKILL_CONFIG
|
|
29
|
+
? JSON.parse(process.env.WALL_E_SKILL_CONFIG) : {};
|
|
30
|
+
if (envConfig.owner_name) ownerName = envConfig.owner_name;
|
|
31
|
+
if (envConfig.depth != null) maxDepth = envConfig.depth;
|
|
32
|
+
} catch { /* ignore */ }
|
|
33
|
+
|
|
34
|
+
function sleep(ms) {
|
|
35
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── Glean MCP ───────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
let mcpClient;
|
|
41
|
+
let gleanServerName;
|
|
42
|
+
|
|
43
|
+
async function initGlean() {
|
|
44
|
+
mcpClient = require(path.resolve(SKILL_DIR, '..', '..', '..', 'skills', 'mcp-client'));
|
|
45
|
+
const configs = mcpClient.loadMcpConfigs();
|
|
46
|
+
gleanServerName = Object.keys(configs).find(k => k.includes('glean'));
|
|
47
|
+
if (!gleanServerName) throw new Error('Glean MCP not configured');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function searchPerson(query) {
|
|
51
|
+
const result = await mcpClient.callMcpTool(gleanServerName, 'employee_search', { query });
|
|
52
|
+
// Extract raw text from MCP response
|
|
53
|
+
let raw;
|
|
54
|
+
if (result && result.content) {
|
|
55
|
+
raw = result.content.map(c => c.text || JSON.stringify(c)).join('\n');
|
|
56
|
+
} else {
|
|
57
|
+
raw = JSON.stringify(result);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Parse JSON response — Glean returns { entities: [{ person: {...} }] }
|
|
61
|
+
try {
|
|
62
|
+
const data = JSON.parse(raw);
|
|
63
|
+
if (data.entities && data.entities.length > 0) {
|
|
64
|
+
return data.entities.map(e => e.person).filter(Boolean);
|
|
65
|
+
}
|
|
66
|
+
} catch { /* not JSON, try text parsing below */ }
|
|
67
|
+
|
|
68
|
+
// Fallback: already an array of person objects
|
|
69
|
+
if (Array.isArray(raw)) return raw;
|
|
70
|
+
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── Build readable profile ──────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
function buildProfile(person, relationship) {
|
|
77
|
+
const parts = [person.name];
|
|
78
|
+
if (person.title) parts.push(`Title: ${person.title}`);
|
|
79
|
+
if (person.department) parts.push(`Department: ${person.department}`);
|
|
80
|
+
if (person.type) parts.push(`Type: ${person.type}`);
|
|
81
|
+
if (person.location) parts.push(`Location: ${person.location}`);
|
|
82
|
+
if (person.email) parts.push(`Email: ${person.email}`);
|
|
83
|
+
if (person.startDate) parts.push(`Start Date: ${person.startDate}`);
|
|
84
|
+
if (person.pronoun && person.pronoun !== 'None') parts.push(`Pronouns: ${person.pronoun}`);
|
|
85
|
+
if (person.manager) {
|
|
86
|
+
const mgr = typeof person.manager === 'object' ? person.manager.name : person.manager;
|
|
87
|
+
parts.push(`Manager: ${mgr}`);
|
|
88
|
+
}
|
|
89
|
+
if (person.directReportsCount) parts.push(`Direct Reports: ${person.directReportsCount}`);
|
|
90
|
+
if (person.totalReportsCount) parts.push(`Total Reports: ${person.totalReportsCount}`);
|
|
91
|
+
if (person.directReports && person.directReports.length > 0) {
|
|
92
|
+
const names = person.directReports.map(r => typeof r === 'object' ? r.name : r);
|
|
93
|
+
parts.push(`Direct Report Names: ${names.join(', ')}`);
|
|
94
|
+
}
|
|
95
|
+
if (person.teams && person.teams.length > 0) {
|
|
96
|
+
parts.push(`Teams: ${person.teams.join(', ')}`);
|
|
97
|
+
}
|
|
98
|
+
if (person.datasourceToProfileLink) {
|
|
99
|
+
const links = person.datasourceToProfileLink;
|
|
100
|
+
if (links.SLACK) parts.push(`Slack: ${links.SLACK}`);
|
|
101
|
+
}
|
|
102
|
+
if (relationship) parts.push(`Relationship: ${relationship}`);
|
|
103
|
+
return parts.join('\n');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Brain storage ───────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
function storePerson(brain, person, relationship) {
|
|
109
|
+
const email = person.email || `unknown-${person.name.replace(/\s+/g, '-').toLowerCase()}`;
|
|
110
|
+
const sourceId = `glean:person:${email}`;
|
|
111
|
+
const content = buildProfile(person, relationship);
|
|
112
|
+
|
|
113
|
+
// Strip circular/large fields from metadata
|
|
114
|
+
const meta = { ...person, relationship };
|
|
115
|
+
// Flatten manager to just name+email
|
|
116
|
+
if (meta.manager && typeof meta.manager === 'object') {
|
|
117
|
+
meta.managerName = meta.manager.name;
|
|
118
|
+
meta.managerEmail = meta.manager.email;
|
|
119
|
+
delete meta.manager;
|
|
120
|
+
}
|
|
121
|
+
// Flatten direct reports to names
|
|
122
|
+
if (meta.directReports) {
|
|
123
|
+
meta.directReportNames = meta.directReports.map(r => typeof r === 'object' ? r.name : r);
|
|
124
|
+
delete meta.directReports;
|
|
125
|
+
}
|
|
126
|
+
const metadata = JSON.stringify(meta);
|
|
127
|
+
|
|
128
|
+
// Check if exists and needs update
|
|
129
|
+
const existing = brain.getDb().prepare(
|
|
130
|
+
'SELECT id, content FROM memories WHERE source = ? AND source_id = ?'
|
|
131
|
+
).get('glean', sourceId);
|
|
132
|
+
|
|
133
|
+
if (existing) {
|
|
134
|
+
if (existing.content !== content) {
|
|
135
|
+
brain.getDb().prepare(
|
|
136
|
+
'UPDATE memories SET content = ?, metadata = ?, timestamp = ? WHERE id = ?'
|
|
137
|
+
).run(content, metadata, new Date().toISOString(), existing.id);
|
|
138
|
+
return 'updated';
|
|
139
|
+
}
|
|
140
|
+
return 'skipped';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const result = brain.insertMemory({
|
|
144
|
+
source: 'glean',
|
|
145
|
+
source_id: sourceId,
|
|
146
|
+
source_channel: person.department || 'unknown',
|
|
147
|
+
memory_type: 'team_member',
|
|
148
|
+
subject: person.name,
|
|
149
|
+
content,
|
|
150
|
+
metadata,
|
|
151
|
+
importance: 0.5,
|
|
152
|
+
timestamp: new Date().toISOString(),
|
|
153
|
+
});
|
|
154
|
+
return result ? 'inserted' : 'skipped';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── Recursive crawl ─────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
async function main() {
|
|
160
|
+
await initGlean();
|
|
161
|
+
|
|
162
|
+
const brainPath = path.resolve(SKILL_DIR, '..', '..', '..', 'brain.js');
|
|
163
|
+
const brain = require(brainPath);
|
|
164
|
+
brain.initDb();
|
|
165
|
+
|
|
166
|
+
// Resolve owner name from brain if not set via env/args
|
|
167
|
+
if (!ownerName) {
|
|
168
|
+
ownerName = brain.getOwnerName() || '';
|
|
169
|
+
}
|
|
170
|
+
if (!ownerName) {
|
|
171
|
+
console.error('[glean-team-sync] No owner name configured (set WALLE_OWNER_NAME or --owner)');
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const stats = { synced: 0, updated: 0, skipped: 0, errors: 0, people: [] };
|
|
176
|
+
const visited = new Set(); // by email
|
|
177
|
+
|
|
178
|
+
async function crawlPerson(name, relationship, depth) {
|
|
179
|
+
console.error(`[glean-team-sync] Searching: ${name} (${relationship}, depth=${depth})`);
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const people = await searchPerson(name);
|
|
183
|
+
await sleep(PAUSE_MS);
|
|
184
|
+
|
|
185
|
+
if (people.length === 0) {
|
|
186
|
+
console.error(`[glean-team-sync] No results for: ${name}`);
|
|
187
|
+
stats.errors++;
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Find best match
|
|
192
|
+
const normalized = name.trim().toLowerCase();
|
|
193
|
+
const person = people.find(p => p.name && p.name.toLowerCase() === normalized)
|
|
194
|
+
|| people.find(p => p.name && p.name.toLowerCase().includes(normalized))
|
|
195
|
+
|| people[0];
|
|
196
|
+
|
|
197
|
+
if (!person || !person.name) {
|
|
198
|
+
console.error(`[glean-team-sync] No valid profile for: ${name}`);
|
|
199
|
+
stats.errors++;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const email = (person.email || '').toLowerCase();
|
|
204
|
+
if (visited.has(email || person.name.toLowerCase())) return;
|
|
205
|
+
visited.add(email || person.name.toLowerCase());
|
|
206
|
+
|
|
207
|
+
// Store
|
|
208
|
+
const result = storePerson(brain, person, relationship);
|
|
209
|
+
if (result === 'inserted') stats.synced++;
|
|
210
|
+
else if (result === 'updated') stats.updated++;
|
|
211
|
+
else stats.skipped++;
|
|
212
|
+
stats.people.push(person.name);
|
|
213
|
+
|
|
214
|
+
console.error(`[glean-team-sync] ${result}: ${person.name} — ${person.title || 'no title'}`);
|
|
215
|
+
|
|
216
|
+
// Recurse into org tree
|
|
217
|
+
if (depth < maxDepth) {
|
|
218
|
+
// Manager
|
|
219
|
+
if (person.manager) {
|
|
220
|
+
const mgrName = typeof person.manager === 'object' ? person.manager.name : person.manager;
|
|
221
|
+
if (mgrName) {
|
|
222
|
+
await crawlPerson(mgrName, `manager of ${person.name}`, depth + 1);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Direct reports
|
|
227
|
+
if (person.directReports && person.directReports.length > 0) {
|
|
228
|
+
for (const report of person.directReports) {
|
|
229
|
+
const reportName = typeof report === 'object' ? report.name : report;
|
|
230
|
+
if (reportName) {
|
|
231
|
+
await crawlPerson(reportName, `reports to ${person.name}`, depth + 1);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
} catch (err) {
|
|
237
|
+
console.error(`[glean-team-sync] Error searching ${name}: ${err.message}`);
|
|
238
|
+
stats.errors++;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
await crawlPerson(ownerName, 'self (owner)', 0);
|
|
243
|
+
|
|
244
|
+
brain.closeDb();
|
|
245
|
+
mcpClient.disconnectAll();
|
|
246
|
+
|
|
247
|
+
console.log(JSON.stringify(stats));
|
|
248
|
+
console.error(`[glean-team-sync] Done: ${stats.synced} new, ${stats.updated} updated, ${stats.skipped} unchanged, ${stats.errors} errors (${stats.people.length} people)`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
main().catch(err => {
|
|
252
|
+
console.error(`[glean-team-sync] Fatal: ${err.message}`);
|
|
253
|
+
process.exit(1);
|
|
254
|
+
});
|
|
@@ -22,7 +22,7 @@ permissions:
|
|
|
22
22
|
|
|
23
23
|
## What This Skill Does
|
|
24
24
|
|
|
25
|
-
Polls Slack for messages mentioning @walle or @wall-e from the owner
|
|
25
|
+
Polls Slack for messages mentioning @walle or @wall-e from the owner.
|
|
26
26
|
Classifies each mention as either a task assignment or a question, then:
|
|
27
27
|
|
|
28
28
|
- **Task**: Creates a brain task with `source: 'slack'` and `source_ref` linking
|