@forwardimpact/outpost 3.0.3
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 +201 -0
- package/README.md +15 -0
- package/bin/fit-outpost.js +3 -0
- package/config/scheduler.json +34 -0
- package/package.json +63 -0
- package/src/agent-runner.js +189 -0
- package/src/index.js +5 -0
- package/src/kb-manager.js +228 -0
- package/src/outpost.js +417 -0
- package/src/scheduler.js +195 -0
- package/src/socket-server.js +336 -0
- package/src/state-manager.js +126 -0
- package/templates/.claude/agents/chief-of-staff.md +58 -0
- package/templates/.claude/agents/concierge.md +46 -0
- package/templates/.claude/agents/head-hunter.md +57 -0
- package/templates/.claude/agents/librarian.md +60 -0
- package/templates/.claude/agents/postman.md +47 -0
- package/templates/.claude/agents/recruiter.md +63 -0
- package/templates/.claude/settings.json +88 -0
- package/templates/.claude/skills/candidate-report/SKILL.md +139 -0
- package/templates/.claude/skills/candidate-report/references/report-template.html +203 -0
- package/templates/.claude/skills/candidate-report/references/report.css +438 -0
- package/templates/.claude/skills/candidate-report/references/rubric.md +76 -0
- package/templates/.claude/skills/candidate-report/scripts/render-pdf.mjs +48 -0
- package/templates/.claude/skills/deck-create/SKILL.md +75 -0
- package/templates/.claude/skills/deck-create/references/slide.css +35 -0
- package/templates/.claude/skills/deck-create/scripts/convert-to-pdf.mjs +47 -0
- package/templates/.claude/skills/deck-summarize/SKILL.md +139 -0
- package/templates/.claude/skills/deck-summarize/references/brief-template.md +85 -0
- package/templates/.claude/skills/deck-summarize/scripts/extract-pptx.mjs +210 -0
- package/templates/.claude/skills/doc-collab/SKILL.md +112 -0
- package/templates/.claude/skills/doc-create/SKILL.md +105 -0
- package/templates/.claude/skills/doc-create/scripts/convert-to-pdf.mjs +50 -0
- package/templates/.claude/skills/draft-emails/SKILL.md +141 -0
- package/templates/.claude/skills/draft-emails/references/template.md +26 -0
- package/templates/.claude/skills/draft-emails/scripts/scan-emails.mjs +66 -0
- package/templates/.claude/skills/draft-emails/scripts/send-email.mjs +139 -0
- package/templates/.claude/skills/extract-entities/SKILL.md +187 -0
- package/templates/.claude/skills/extract-entities/references/TEMPLATES.md +14 -0
- package/templates/.claude/skills/extract-entities/references/conditions.md +55 -0
- package/templates/.claude/skills/extract-entities/references/content.md +104 -0
- package/templates/.claude/skills/extract-entities/references/links.md +48 -0
- package/templates/.claude/skills/extract-entities/references/recruitment.md +57 -0
- package/templates/.claude/skills/extract-entities/references/resolution.md +71 -0
- package/templates/.claude/skills/extract-entities/references/sources.md +70 -0
- package/templates/.claude/skills/extract-entities/references/templates-conditions.md +36 -0
- package/templates/.claude/skills/extract-entities/references/templates-goals-priorities.md +63 -0
- package/templates/.claude/skills/extract-entities/references/templates-people-orgs.md +67 -0
- package/templates/.claude/skills/extract-entities/references/templates-projects-topics.md +68 -0
- package/templates/.claude/skills/extract-entities/scripts/state.mjs +131 -0
- package/templates/.claude/skills/hyprnote-follow/SKILL.md +174 -0
- package/templates/.claude/skills/hyprnote-follow/references/coaching.md +118 -0
- package/templates/.claude/skills/hyprnote-follow/references/debrief.md +59 -0
- package/templates/.claude/skills/hyprnote-follow/references/meeting-types.md +14 -0
- package/templates/.claude/skills/hyprnote-follow/scripts/follow.mjs +243 -0
- package/templates/.claude/skills/hyprnote-process/SKILL.md +147 -0
- package/templates/.claude/skills/hyprnote-process/references/extraction.md +84 -0
- package/templates/.claude/skills/hyprnote-process/references/sessions.md +81 -0
- package/templates/.claude/skills/hyprnote-process/scripts/scan.mjs +246 -0
- package/templates/.claude/skills/hyprnote-trim/SKILL.md +177 -0
- package/templates/.claude/skills/manage-tasks/SKILL.md +152 -0
- package/templates/.claude/skills/manage-tasks/references/format.md +73 -0
- package/templates/.claude/skills/meeting-prep/SKILL.md +180 -0
- package/templates/.claude/skills/organize-files/SKILL.md +146 -0
- package/templates/.claude/skills/organize-files/scripts/organize-by-type.mjs +105 -0
- package/templates/.claude/skills/organize-files/scripts/summarize.mjs +91 -0
- package/templates/.claude/skills/req-assess/SKILL.md +137 -0
- package/templates/.claude/skills/req-assess/references/interview-template.md +62 -0
- package/templates/.claude/skills/req-assess/references/panel-template.md +117 -0
- package/templates/.claude/skills/req-assess/references/rubric.md +65 -0
- package/templates/.claude/skills/req-decide/SKILL.md +163 -0
- package/templates/.claude/skills/req-decide/references/rubric.md +63 -0
- package/templates/.claude/skills/req-decide/references/template.md +118 -0
- package/templates/.claude/skills/req-forget/SKILL.md +154 -0
- package/templates/.claude/skills/req-forget/references/classify.md +23 -0
- package/templates/.claude/skills/req-forget/references/locations.md +59 -0
- package/templates/.claude/skills/req-forget/references/report-template.md +67 -0
- package/templates/.claude/skills/req-scan/SKILL.md +145 -0
- package/templates/.claude/skills/req-scan/references/fallbacks.md +76 -0
- package/templates/.claude/skills/req-scan/references/filters.md +83 -0
- package/templates/.claude/skills/req-scan/references/sources.md +76 -0
- package/templates/.claude/skills/req-scan/references/state.md +54 -0
- package/templates/.claude/skills/req-scan/references/template.md +36 -0
- package/templates/.claude/skills/req-scan/scripts/state.mjs +396 -0
- package/templates/.claude/skills/req-screen/SKILL.md +151 -0
- package/templates/.claude/skills/req-screen/references/rubric.md +94 -0
- package/templates/.claude/skills/req-screen/references/template.md +91 -0
- package/templates/.claude/skills/req-track/SKILL.md +189 -0
- package/templates/.claude/skills/req-track/references/fields.md +114 -0
- package/templates/.claude/skills/req-track/references/signals.md +42 -0
- package/templates/.claude/skills/req-track/references/statuses.md +32 -0
- package/templates/.claude/skills/req-track/references/templates.md +127 -0
- package/templates/.claude/skills/req-workday/SKILL.md +162 -0
- package/templates/.claude/skills/req-workday/references/field-mapping.md +40 -0
- package/templates/.claude/skills/req-workday/references/status-mapping.md +38 -0
- package/templates/.claude/skills/req-workday/references/templates.md +126 -0
- package/templates/.claude/skills/req-workday/references/xlsx-format.md +56 -0
- package/templates/.claude/skills/req-workday/scripts/parse-workday.mjs +313 -0
- package/templates/.claude/skills/send-chat/SKILL.md +170 -0
- package/templates/.claude/skills/sync-apple-calendar/SKILL.md +145 -0
- package/templates/.claude/skills/sync-apple-calendar/references/SCHEMA.md +80 -0
- package/templates/.claude/skills/sync-apple-calendar/scripts/query.mjs +307 -0
- package/templates/.claude/skills/sync-apple-calendar/scripts/sync.mjs +332 -0
- package/templates/.claude/skills/sync-apple-mail/SKILL.md +151 -0
- package/templates/.claude/skills/sync-apple-mail/references/SCHEMA.md +115 -0
- package/templates/.claude/skills/sync-apple-mail/scripts/parse-emlx.mjs +389 -0
- package/templates/.claude/skills/sync-apple-mail/scripts/sync-helpers.mjs +372 -0
- package/templates/.claude/skills/sync-apple-mail/scripts/sync.mjs +239 -0
- package/templates/.claude/skills/sync-teams/SKILL.md +177 -0
- package/templates/.claude/skills/sync-teams/scripts/idb-reader.mjs +147 -0
- package/templates/.claude/skills/sync-teams/scripts/leveldb-reader.mjs +288 -0
- package/templates/.claude/skills/sync-teams/scripts/sync.mjs +381 -0
- package/templates/.claude/skills/upstream-skill/SKILL.md +130 -0
- package/templates/.claude/skills/upstream-skill/references/examples.md +80 -0
- package/templates/.claude/skills/weekly-update/SKILL.md +141 -0
- package/templates/.claude/skills/weekly-update/references/template.md +55 -0
- package/templates/CLAUDE.md +134 -0
- package/templates/USER.md +3 -0
- package/templates/apm.yml +5 -0
- package/templates/knowledge/Briefings/.gitkeep +0 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Parse a Workday requisition export (.xlsx) and output structured JSON.
|
|
4
|
+
*
|
|
5
|
+
* Reads Sheet1 for requisition metadata and the "Candidates" sheet for
|
|
6
|
+
* candidate data. Outputs a JSON object to stdout with:
|
|
7
|
+
* - requisition: { id, title, startDate, targetHireDate, location,
|
|
8
|
+
* hiringManager, recruiter }
|
|
9
|
+
* - candidates: [ { name, cleanName, stage, step, resumeFile, dateApplied,
|
|
10
|
+
* currentTitle, currentCompany, source, referredBy,
|
|
11
|
+
* availabilityDate, visaRequirement, eligibleToWork,
|
|
12
|
+
* relocation, salaryExpectations, nonCompete, location,
|
|
13
|
+
* phone, email, totalYearsExperience, allJobTitles,
|
|
14
|
+
* companies, degrees, fieldsOfStudy, language,
|
|
15
|
+
* resumeText, internalExternal } ]
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* bun scripts/parse-workday.mjs <path-to-xlsx>
|
|
19
|
+
* bun scripts/parse-workday.mjs <path-to-xlsx> --summary
|
|
20
|
+
* bun scripts/parse-workday.mjs -h|--help
|
|
21
|
+
*
|
|
22
|
+
* Requires: bun install read-excel-file
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
if (
|
|
26
|
+
process.argv.includes("-h") ||
|
|
27
|
+
process.argv.includes("--help") ||
|
|
28
|
+
process.argv.length < 3
|
|
29
|
+
) {
|
|
30
|
+
console.log(`parse-workday — extract candidates from a Workday requisition export
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
bun scripts/parse-workday.mjs <path-to-xlsx> Full JSON output
|
|
34
|
+
bun scripts/parse-workday.mjs <path-to-xlsx> --summary Name + status only
|
|
35
|
+
bun scripts/parse-workday.mjs -h|--help Show this help
|
|
36
|
+
|
|
37
|
+
Output (JSON):
|
|
38
|
+
{ requisition: { id, title, ... }, candidates: [ { name, ... }, ... ] }
|
|
39
|
+
|
|
40
|
+
Requires: bun install read-excel-file`);
|
|
41
|
+
process.exit(process.argv.length < 3 ? 1 : 0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let readXlsxFile;
|
|
45
|
+
try {
|
|
46
|
+
readXlsxFile = (await import("read-excel-file/node")).default;
|
|
47
|
+
} catch {
|
|
48
|
+
console.error(
|
|
49
|
+
"Error: read-excel-file package not found. Install it first:\n bun install read-excel-file",
|
|
50
|
+
);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const filePath = process.argv[2];
|
|
55
|
+
const summaryMode = process.argv.includes("--summary");
|
|
56
|
+
|
|
57
|
+
/** Read a sheet by number (1-indexed) or name, returning rows as arrays of strings. */
|
|
58
|
+
async function readSheet(file, sheet) {
|
|
59
|
+
const rows = await readXlsxFile(file, { sheet });
|
|
60
|
+
// Normalise null cells to empty strings to match previous behaviour
|
|
61
|
+
return rows.map((row) => row.map((cell) => (cell == null ? "" : cell)));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Get sheet names to find the candidates sheet
|
|
65
|
+
const sheets = await readXlsxFile(filePath, { getSheets: true });
|
|
66
|
+
const sheetNames = sheets.map((s) => s.name);
|
|
67
|
+
|
|
68
|
+
// --- Sheet 1: Requisition metadata ---
|
|
69
|
+
|
|
70
|
+
const sheet1Rows = await readSheet(filePath, 1);
|
|
71
|
+
|
|
72
|
+
/** Extract the requisition ID and title from the header row. */
|
|
73
|
+
function parseReqHeader(headerText) {
|
|
74
|
+
// Format: "4951493 Principal Software Engineer – Forward Deployed: 4951493 ..."
|
|
75
|
+
const text = String(headerText).split(":")[0].trim();
|
|
76
|
+
const match = text.match(/^(\d+)\s+(.+)$/);
|
|
77
|
+
if (match) return { id: match[1], title: match[2] };
|
|
78
|
+
return { id: "", title: text };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Build a key-value map from Sheet1 rows (column A = label, column B = value). */
|
|
82
|
+
function buildReqMetadata(rows) {
|
|
83
|
+
const meta = {};
|
|
84
|
+
for (const row of rows) {
|
|
85
|
+
const key = String(row[0] || "").trim();
|
|
86
|
+
const val = String(row[1] || "").trim();
|
|
87
|
+
if (key && val) meta[key] = val;
|
|
88
|
+
}
|
|
89
|
+
return meta;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const reqHeader = parseReqHeader(sheet1Rows[0]?.[0] || "");
|
|
93
|
+
const reqMeta = buildReqMetadata(sheet1Rows.slice(1));
|
|
94
|
+
|
|
95
|
+
/** Clean a metadata date string (e.g. "02/10/2026 - 22 days ago" → "2026-02-10"). */
|
|
96
|
+
function cleanMetaDate(val) {
|
|
97
|
+
if (!val) return "";
|
|
98
|
+
const clean = val.replace(/\s*-\s*\d+\s+days?\s+ago$/i, "").trim();
|
|
99
|
+
// Convert MM/DD/YYYY → YYYY-MM-DD
|
|
100
|
+
const match = clean.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
|
|
101
|
+
if (match) return `${match[3]}-${match[1]}-${match[2]}`;
|
|
102
|
+
return clean;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const requisition = {
|
|
106
|
+
id: reqHeader.id,
|
|
107
|
+
title: reqHeader.title,
|
|
108
|
+
startDate: cleanMetaDate(reqMeta["Recruiting Start Date"]),
|
|
109
|
+
targetHireDate: cleanMetaDate(reqMeta["Target Hire Date"]),
|
|
110
|
+
location: reqMeta["Primary Location"] || "",
|
|
111
|
+
hiringManager: reqMeta["Hiring Manager"] || "",
|
|
112
|
+
recruiter: reqMeta["Recruiter"] || "",
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// --- Candidates sheet ---
|
|
116
|
+
|
|
117
|
+
// Find the candidates sheet. Workday exports vary:
|
|
118
|
+
// - Old format: 3+ sheets, candidates on a sheet named "Candidates" or Sheet3
|
|
119
|
+
// - New format: 2 sheets, candidates on Sheet2
|
|
120
|
+
const candSheetName =
|
|
121
|
+
sheetNames.find((n) => n.toLowerCase() === "candidates") ||
|
|
122
|
+
sheetNames[Math.min(2, sheetNames.length - 1)];
|
|
123
|
+
const candRows = await readSheet(filePath, candSheetName);
|
|
124
|
+
|
|
125
|
+
// Find the header row dynamically — look for a row containing "Stage"
|
|
126
|
+
// Old format: row 3 (index 2). New format: row 8 (index 7).
|
|
127
|
+
let HEADER_ROW = 2;
|
|
128
|
+
for (let i = 0; i < Math.min(15, candRows.length); i++) {
|
|
129
|
+
if (candRows[i].some((c) => String(c).trim().toLowerCase() === "stage")) {
|
|
130
|
+
HEADER_ROW = i;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const DATA_START = HEADER_ROW + 1;
|
|
135
|
+
|
|
136
|
+
// --- Build header-driven column index map ---
|
|
137
|
+
// Column layout varies between Workday exports (extra columns like "Jobs Applied to"
|
|
138
|
+
// or "Referred by" shift indices). Map by header name to be resilient.
|
|
139
|
+
|
|
140
|
+
const headerRow = candRows[HEADER_ROW] || [];
|
|
141
|
+
const colMap = {};
|
|
142
|
+
const HEADER_ALIASES = {
|
|
143
|
+
"job application": "name", // second "Job Application" column (index 1) has candidate name
|
|
144
|
+
stage: "stage",
|
|
145
|
+
"step / disposition": "step",
|
|
146
|
+
"awaiting me": "awaitingMe",
|
|
147
|
+
"awaiting action": "awaitingAction",
|
|
148
|
+
resume: "resumeFile",
|
|
149
|
+
"date applied": "dateApplied",
|
|
150
|
+
"current job title": "currentTitle",
|
|
151
|
+
"current company": "currentCompany",
|
|
152
|
+
source: "source",
|
|
153
|
+
"referred by": "referredBy",
|
|
154
|
+
"availability date": "availabilityDate",
|
|
155
|
+
"visa requirement": "visaRequirement",
|
|
156
|
+
"eligible to work": "eligibleToWork",
|
|
157
|
+
relocation: "relocation",
|
|
158
|
+
"salary expectations": "salaryExpectations",
|
|
159
|
+
"non-compete": "nonCompete",
|
|
160
|
+
"candidate location": "location",
|
|
161
|
+
phone: "phone",
|
|
162
|
+
email: "email",
|
|
163
|
+
"total years experience": "totalYearsExperience",
|
|
164
|
+
"all job titles": "allJobTitles",
|
|
165
|
+
companies: "companies",
|
|
166
|
+
degrees: "degrees",
|
|
167
|
+
"fields of study": "fieldsOfStudy",
|
|
168
|
+
language: "language",
|
|
169
|
+
"resume text": "resumeText",
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// Skip columns we don't need (e.g. "Jobs Applied to", "Create Candidate Home Account URL")
|
|
173
|
+
for (let i = 0; i < headerRow.length; i++) {
|
|
174
|
+
const hdr = String(headerRow[i]).trim().toLowerCase();
|
|
175
|
+
const field = HEADER_ALIASES[hdr];
|
|
176
|
+
if (field) {
|
|
177
|
+
// "Job Application" appears twice (cols A and B) — always take the latest
|
|
178
|
+
// occurrence so we end up with the second one (index 1) which has the name
|
|
179
|
+
colMap[field] = i;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Fallback: if "name" wasn't mapped, use index 0 (new format) or 1 (old format)
|
|
184
|
+
if (colMap.name === undefined) colMap.name = 1;
|
|
185
|
+
// In new format there's only one "Job Application" column (index 0) — the
|
|
186
|
+
// "always take latest" logic already handles this correctly.
|
|
187
|
+
|
|
188
|
+
/** Get a cell value by field name, with fallback to empty string. */
|
|
189
|
+
function col(row, field) {
|
|
190
|
+
const idx = colMap[field];
|
|
191
|
+
if (idx === undefined) return "";
|
|
192
|
+
return row[idx] ?? "";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Clean a candidate name by stripping annotations like (Prior Worker),
|
|
197
|
+
* (Internal), etc. Returns { cleanName, internalExternal }.
|
|
198
|
+
*/
|
|
199
|
+
function parseName(raw) {
|
|
200
|
+
const name = String(raw).trim();
|
|
201
|
+
if (!name) return { cleanName: "", internalExternal: "" };
|
|
202
|
+
|
|
203
|
+
const match = name.match(/^(.+?)\s*\(([^)]+)\)\s*$/);
|
|
204
|
+
if (match) {
|
|
205
|
+
const annotation = match[2].trim();
|
|
206
|
+
let ie;
|
|
207
|
+
if (/prior\s*worker/i.test(annotation)) ie = "External (Prior Worker)";
|
|
208
|
+
else if (/internal/i.test(annotation)) ie = "Internal";
|
|
209
|
+
else ie = annotation;
|
|
210
|
+
return { cleanName: match[1].trim(), internalExternal: ie };
|
|
211
|
+
}
|
|
212
|
+
return { cleanName: name, internalExternal: "" };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Detect source-based internal/external when name annotation is absent. */
|
|
216
|
+
function inferInternalExternal(source, nameAnnotation) {
|
|
217
|
+
if (nameAnnotation) return nameAnnotation;
|
|
218
|
+
if (/internal/i.test(source)) return "Internal";
|
|
219
|
+
return "External";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Format a date value (may be Date object or string). */
|
|
223
|
+
function fmtDate(val) {
|
|
224
|
+
if (!val) return "";
|
|
225
|
+
if (val instanceof Date) {
|
|
226
|
+
// Use local date parts to avoid UTC offset shifting the day
|
|
227
|
+
const y = val.getFullYear();
|
|
228
|
+
const m = String(val.getMonth() + 1).padStart(2, "0");
|
|
229
|
+
const d = String(val.getDate()).padStart(2, "0");
|
|
230
|
+
return `${y}-${m}-${d}`;
|
|
231
|
+
}
|
|
232
|
+
const s = String(val).trim();
|
|
233
|
+
// Strip trailing " 00:00:00" and relative text like " - 22 days ago"
|
|
234
|
+
return s
|
|
235
|
+
.replace(/\s+\d{2}:\d{2}:\d{2}$/, "")
|
|
236
|
+
.replace(/\s*-\s*\d+\s+days?\s+ago$/i, "");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Normalise multiline cell values into clean lists. */
|
|
240
|
+
function multiline(val) {
|
|
241
|
+
if (!val) return "";
|
|
242
|
+
return String(val)
|
|
243
|
+
.split("\n")
|
|
244
|
+
.map((l) => l.trim())
|
|
245
|
+
.filter(Boolean)
|
|
246
|
+
.join(", ");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const candidates = [];
|
|
250
|
+
|
|
251
|
+
for (let i = DATA_START; i < candRows.length; i++) {
|
|
252
|
+
const row = candRows[i];
|
|
253
|
+
const rawName = String(col(row, "name") || "").trim();
|
|
254
|
+
const stage = String(col(row, "stage") || "").trim();
|
|
255
|
+
|
|
256
|
+
// Skip empty rows; stop at stage-summary rows (name present but no stage)
|
|
257
|
+
if (!rawName) continue;
|
|
258
|
+
if (!stage) break;
|
|
259
|
+
|
|
260
|
+
const { cleanName, internalExternal: nameIE } = parseName(rawName);
|
|
261
|
+
const source = String(col(row, "source") || "").trim();
|
|
262
|
+
|
|
263
|
+
candidates.push({
|
|
264
|
+
name: rawName,
|
|
265
|
+
cleanName,
|
|
266
|
+
stage,
|
|
267
|
+
step: String(col(row, "step") || "").trim(),
|
|
268
|
+
awaitingMe: String(col(row, "awaitingMe") || "").trim(),
|
|
269
|
+
awaitingAction: String(col(row, "awaitingAction") || "").trim(),
|
|
270
|
+
resumeFile: String(col(row, "resumeFile") || "").trim(),
|
|
271
|
+
dateApplied: fmtDate(col(row, "dateApplied")),
|
|
272
|
+
currentTitle: String(col(row, "currentTitle") || "").trim(),
|
|
273
|
+
currentCompany: String(col(row, "currentCompany") || "").trim(),
|
|
274
|
+
source,
|
|
275
|
+
referredBy: String(col(row, "referredBy") || "").trim(),
|
|
276
|
+
availabilityDate: fmtDate(col(row, "availabilityDate")),
|
|
277
|
+
visaRequirement: String(col(row, "visaRequirement") || "").trim(),
|
|
278
|
+
eligibleToWork: String(col(row, "eligibleToWork") || "").trim(),
|
|
279
|
+
relocation: String(col(row, "relocation") || "").trim(),
|
|
280
|
+
salaryExpectations: String(col(row, "salaryExpectations") || "").trim(),
|
|
281
|
+
nonCompete: String(col(row, "nonCompete") || "").trim(),
|
|
282
|
+
location: String(col(row, "location") || "").trim(),
|
|
283
|
+
phone: String(col(row, "phone") || "").trim(),
|
|
284
|
+
email: String(col(row, "email") || "").trim(),
|
|
285
|
+
totalYearsExperience: String(col(row, "totalYearsExperience") || "").trim(),
|
|
286
|
+
allJobTitles: multiline(col(row, "allJobTitles")),
|
|
287
|
+
companies: multiline(col(row, "companies")),
|
|
288
|
+
degrees: multiline(col(row, "degrees")),
|
|
289
|
+
fieldsOfStudy: multiline(col(row, "fieldsOfStudy")),
|
|
290
|
+
language: multiline(col(row, "language")),
|
|
291
|
+
resumeText: String(col(row, "resumeText") || "").trim(),
|
|
292
|
+
internalExternal: inferInternalExternal(source, nameIE),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// --- Output ---
|
|
297
|
+
|
|
298
|
+
if (summaryMode) {
|
|
299
|
+
console.log(`Requisition: ${requisition.id} — ${requisition.title}`);
|
|
300
|
+
console.log(`Location: ${requisition.location}`);
|
|
301
|
+
console.log(`Hiring Manager: ${requisition.hiringManager}`);
|
|
302
|
+
console.log(`Recruiter: ${requisition.recruiter}`);
|
|
303
|
+
console.log(`Candidates: ${candidates.length}`);
|
|
304
|
+
console.log();
|
|
305
|
+
for (const c of candidates) {
|
|
306
|
+
const resume = c.resumeText ? "has resume" : "no resume";
|
|
307
|
+
console.log(
|
|
308
|
+
` ${c.cleanName} — ${c.step || c.stage} (${c.internalExternal}, ${resume})`,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
console.log(JSON.stringify({ requisition, candidates }, null, 2));
|
|
313
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: send-chat
|
|
3
|
+
description: Send messages to people via chat platforms (e.g. Microsoft Teams, Slack) using browser automation. Resolves people by name using the knowledge graph, drafts messages for approval, and sends via the web app. Use when the user asks to message, ping, or chat with someone.
|
|
4
|
+
compatibility:
|
|
5
|
+
requires:
|
|
6
|
+
- browser-automation
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Send Chat
|
|
10
|
+
|
|
11
|
+
Send chat messages to people using browser automation against a web-based chat
|
|
12
|
+
platform (Microsoft Teams, Slack, or similar). Resolves recipients by name from
|
|
13
|
+
the knowledge graph so the user can say "message Sarah about the standup"
|
|
14
|
+
without needing exact display names.
|
|
15
|
+
|
|
16
|
+
## Trigger
|
|
17
|
+
|
|
18
|
+
Run when the user asks to:
|
|
19
|
+
|
|
20
|
+
- Send a message on Teams / Slack / chat
|
|
21
|
+
- Ping / chat / DM someone
|
|
22
|
+
- Follow up with someone via chat
|
|
23
|
+
- Send a message about a topic
|
|
24
|
+
|
|
25
|
+
## Prerequisites
|
|
26
|
+
|
|
27
|
+
- Chat platform web app open and authenticated in the browser
|
|
28
|
+
- Browser automation available (e.g. Chrome MCP, Playwright)
|
|
29
|
+
- Knowledge base populated with people notes
|
|
30
|
+
|
|
31
|
+
## Critical: Always Look Up Context First
|
|
32
|
+
|
|
33
|
+
**BEFORE messaging anyone, you MUST look up the person in the knowledge base.**
|
|
34
|
+
|
|
35
|
+
When the user mentions ANY person:
|
|
36
|
+
|
|
37
|
+
1. **STOP** — Do not open the chat platform yet
|
|
38
|
+
2. **SEARCH** — Look them up: `rg -l "{name}" knowledge/People/`
|
|
39
|
+
3. **READ** — Read their note to understand context, role, recent interactions
|
|
40
|
+
4. **UNDERSTAND** — Know who they are, what you've been working on together
|
|
41
|
+
5. **THEN PROCEED** — Only now compose the message and use browser automation
|
|
42
|
+
|
|
43
|
+
This context is essential for:
|
|
44
|
+
|
|
45
|
+
- Finding the right person if the name is ambiguous
|
|
46
|
+
- Drafting an appropriate message if the user gave a loose prompt
|
|
47
|
+
- Knowing the person's role and relationship for tone
|
|
48
|
+
|
|
49
|
+
## Resolving People
|
|
50
|
+
|
|
51
|
+
The user will refer to people by first name, last name, or nickname. Resolve to
|
|
52
|
+
a full name using the knowledge graph:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Find person by partial name
|
|
56
|
+
rg -l -i "{name}" knowledge/People/
|
|
57
|
+
|
|
58
|
+
# If ambiguous, read candidates to disambiguate
|
|
59
|
+
cat "knowledge/People/{Candidate}.md"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**If ambiguous** (multiple matches), ask the user which person they mean — list
|
|
63
|
+
the matches with roles/orgs to help them pick.
|
|
64
|
+
|
|
65
|
+
**If no match**, tell the user you don't have this person in the knowledge base
|
|
66
|
+
and ask for their full name as it appears in the chat platform.
|
|
67
|
+
|
|
68
|
+
## Composing the Message
|
|
69
|
+
|
|
70
|
+
**Every message MUST be drafted as a text file first.** This ensures the user
|
|
71
|
+
can review and edit the exact message before it's sent.
|
|
72
|
+
|
|
73
|
+
### Draft Workflow
|
|
74
|
+
|
|
75
|
+
1. **Compose the message** based on context and user intent.
|
|
76
|
+
2. **Write it to a draft file** at `drafts/chat-{recipient-slug}-{date}.md`
|
|
77
|
+
- `{recipient-slug}` = lowercase, hyphenated full name (e.g. `sarah-chen`)
|
|
78
|
+
- `{date}` = ISO date (e.g. `2026-02-19`)
|
|
79
|
+
3. **Show the user the draft** — display the file path and contents.
|
|
80
|
+
4. **Wait for approval** — the user may edit the file or ask for changes.
|
|
81
|
+
5. **Only after approval**, proceed to send.
|
|
82
|
+
|
|
83
|
+
**Draft file format:**
|
|
84
|
+
|
|
85
|
+
```markdown
|
|
86
|
+
To: {Full Name}
|
|
87
|
+
Via: {Platform name}
|
|
88
|
+
Date: {YYYY-MM-DD}
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
{message body}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The message body (everything below the `---` separator) is what gets pasted into
|
|
96
|
+
the chat.
|
|
97
|
+
|
|
98
|
+
**Message guidelines:**
|
|
99
|
+
|
|
100
|
+
- Match the user's usual tone — casual for peers, professional for leadership
|
|
101
|
+
- Keep it concise — chat is informal, not email
|
|
102
|
+
- Reference specific context naturally (project names, recent decisions)
|
|
103
|
+
- If the user provides exact wording, use it verbatim
|
|
104
|
+
- If the user said "ping {name}" without detail, ask what they want to say
|
|
105
|
+
- Draft one message based on context — don't offer multiple options
|
|
106
|
+
- **Keep messages on a single line with no formatting.** No line breaks, no
|
|
107
|
+
markdown. Use inline separators (e.g. `•`, `—`) to keep structure. Multi-line
|
|
108
|
+
formatting is unreliable via browser automation.
|
|
109
|
+
|
|
110
|
+
## Browser Automation Flow
|
|
111
|
+
|
|
112
|
+
Once the user has approved the draft, send it as a **single submission** — paste
|
|
113
|
+
the entire message at once rather than typing line by line.
|
|
114
|
+
|
|
115
|
+
### Step 1: Identify the Chat Platform
|
|
116
|
+
|
|
117
|
+
Check which platform is available:
|
|
118
|
+
|
|
119
|
+
- Look for an open tab matching the configured chat URL
|
|
120
|
+
- If no tab is open, ask the user which platform to use and navigate to it
|
|
121
|
+
|
|
122
|
+
### Step 2: Open a Chat with the Recipient
|
|
123
|
+
|
|
124
|
+
1. Use the platform's search or "New chat" feature
|
|
125
|
+
2. Type the recipient's full name
|
|
126
|
+
3. Wait for search results to populate (take a screenshot to verify)
|
|
127
|
+
4. Click the correct person from the results
|
|
128
|
+
|
|
129
|
+
If the person doesn't appear in search, inform the user — they may not be in the
|
|
130
|
+
same organization.
|
|
131
|
+
|
|
132
|
+
### Step 3: Send the Approved Message
|
|
133
|
+
|
|
134
|
+
1. Read the approved draft file to get the message body (below the `---`)
|
|
135
|
+
2. Click the message compose box
|
|
136
|
+
3. Paste the entire message as a single submission
|
|
137
|
+
4. Press Enter or click Send
|
|
138
|
+
5. Take a screenshot to confirm the message was sent
|
|
139
|
+
|
|
140
|
+
### Step 4: Update Knowledge Graph (Optional)
|
|
141
|
+
|
|
142
|
+
If the message is substantive (not just "hey" or "thanks"), note the interaction
|
|
143
|
+
on the person's knowledge note:
|
|
144
|
+
|
|
145
|
+
```markdown
|
|
146
|
+
- {YYYY-MM-DD}: Messaged on {Platform} re: {topic}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Error Handling
|
|
150
|
+
|
|
151
|
+
- **Platform not loaded / auth required:** Tell the user to sign in first, then
|
|
152
|
+
retry
|
|
153
|
+
- **Person not found in search:** Report back — they may be external or using a
|
|
154
|
+
different display name. Ask the user for the exact name
|
|
155
|
+
- **Chat already open:** If a chat with this person is already visible, use it
|
|
156
|
+
directly
|
|
157
|
+
- **UI not as expected:** Take a screenshot and describe what you see. Don't
|
|
158
|
+
click blindly
|
|
159
|
+
|
|
160
|
+
## Constraints
|
|
161
|
+
|
|
162
|
+
- **Always confirm before sending.** Never send a message without explicit user
|
|
163
|
+
approval — this is a hard requirement
|
|
164
|
+
- **One message at a time.** Don't batch-send to multiple people without
|
|
165
|
+
confirming each one
|
|
166
|
+
- **No file attachments.** This skill handles text messages only
|
|
167
|
+
- **No group chats.** Targets 1:1 chats only
|
|
168
|
+
- **No message deletion or editing.** Once sent, it's sent
|
|
169
|
+
- **Respect ethics rules.** Never send messages that contain personal judgments,
|
|
170
|
+
gossip, or sensitive information per the knowledge base ethics policy
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sync-apple-calendar
|
|
3
|
+
description: Sync calendar events from the macOS Calendar app's local SQLite database into ~/.cache/fit/outpost/apple_calendar/ as JSON files. Use on a schedule or when the user asks to sync their calendar. Requires macOS with Calendar app configured and Full Disk Access granted.
|
|
4
|
+
compatibility: Requires macOS with Apple Calendar configured and Full Disk Access granted to the terminal
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Sync Apple Calendar
|
|
8
|
+
|
|
9
|
+
Sync calendar events from the macOS Calendar app's local SQLite database into
|
|
10
|
+
`~/.cache/fit/outpost/apple_calendar/` as JSON files. This is an automated data
|
|
11
|
+
pipeline skill — it ingests raw calendar data that other skills (like
|
|
12
|
+
`extract-entities` and `meeting-prep`) consume downstream.
|
|
13
|
+
|
|
14
|
+
## Trigger
|
|
15
|
+
|
|
16
|
+
Run this skill on a schedule (every 5 minutes) or when the user asks to sync
|
|
17
|
+
their calendar.
|
|
18
|
+
|
|
19
|
+
## Prerequisites
|
|
20
|
+
|
|
21
|
+
- macOS with the built-in Calendar app configured
|
|
22
|
+
- Full Disk Access granted to the terminal (System Settings → Privacy & Security
|
|
23
|
+
→ Full Disk Access)
|
|
24
|
+
|
|
25
|
+
## Inputs
|
|
26
|
+
|
|
27
|
+
- `~/Library/Group Containers/group.com.apple.calendar/Calendar.sqlitedb` —
|
|
28
|
+
Apple Calendar SQLite database (Sonoma+/macOS 14+)
|
|
29
|
+
- `~/Library/Calendars/Calendar.sqlitedb` — fallback path for older macOS
|
|
30
|
+
|
|
31
|
+
## Outputs
|
|
32
|
+
|
|
33
|
+
- `~/.cache/fit/outpost/apple_calendar/{event_id}.json` — one JSON file per
|
|
34
|
+
event (14-day sliding window)
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Implementation
|
|
39
|
+
|
|
40
|
+
Run the sync as a single Node.js script with embedded SQLite. This avoids N+1
|
|
41
|
+
process invocations (one per event for attendees) and handles all data
|
|
42
|
+
transformation in one pass:
|
|
43
|
+
|
|
44
|
+
node scripts/sync.mjs [--days N]
|
|
45
|
+
|
|
46
|
+
- `--days N` — how many days back to sync (default: 30)
|
|
47
|
+
|
|
48
|
+
The script:
|
|
49
|
+
|
|
50
|
+
1. Finds the Calendar database (Sonoma+ path first, then fallback)
|
|
51
|
+
2. Queries all events in a sliding window (`--days` past / 14 days future) with
|
|
52
|
+
a single SQL query
|
|
53
|
+
3. Batch-fetches all attendees for those events in one query
|
|
54
|
+
4. Writes one JSON file per event to `~/.cache/fit/outpost/apple_calendar/`
|
|
55
|
+
5. Cleans up JSON files for events now outside the window
|
|
56
|
+
6. Reports summary (events synced, files cleaned up)
|
|
57
|
+
|
|
58
|
+
## Database Schema
|
|
59
|
+
|
|
60
|
+
See [references/SCHEMA.md](references/SCHEMA.md) for the complete Apple Calendar
|
|
61
|
+
SQLite schema including table structures, column names, and important caveats
|
|
62
|
+
(e.g., Identity uses `address` not `email`, Participant has no `display_name`).
|
|
63
|
+
|
|
64
|
+
## Output Format
|
|
65
|
+
|
|
66
|
+
Each `{event_id}.json` file:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"id": "apple_cal_123",
|
|
71
|
+
"summary": "Meeting with Sarah Chen",
|
|
72
|
+
"start": { "dateTime": "2025-02-12T15:00:00+01:00", "timeZone": "Europe/Paris" },
|
|
73
|
+
"end": { "dateTime": "2025-02-12T16:00:00+01:00", "timeZone": "Europe/Paris" },
|
|
74
|
+
"allDay": false,
|
|
75
|
+
"location": "Zoom",
|
|
76
|
+
"description": "Discuss Q2 roadmap",
|
|
77
|
+
"conferenceUrl": null,
|
|
78
|
+
"calendar": "Calendar",
|
|
79
|
+
"organizer": { "email": "sarah@acme.com", "name": "Chen, Sarah" },
|
|
80
|
+
"attendees": [
|
|
81
|
+
{
|
|
82
|
+
"email": "sarah@acme.com",
|
|
83
|
+
"name": "Chen, Sarah",
|
|
84
|
+
"status": "accepted",
|
|
85
|
+
"role": "required",
|
|
86
|
+
"self": false
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Error Handling
|
|
93
|
+
|
|
94
|
+
- Database not found → Calendar not configured, report and stop
|
|
95
|
+
- Permission denied → Full Disk Access not granted, report and stop
|
|
96
|
+
- Database locked → wait 2 seconds, retry once
|
|
97
|
+
- Skip events with no summary (likely cancelled or placeholder)
|
|
98
|
+
|
|
99
|
+
## Querying Events
|
|
100
|
+
|
|
101
|
+
After syncing, use the query script to filter events by date or time window.
|
|
102
|
+
**Agents should use this script instead of writing bespoke calendar parsers.**
|
|
103
|
+
|
|
104
|
+
node scripts/query.mjs [options]
|
|
105
|
+
|
|
106
|
+
### Time filters (combinable)
|
|
107
|
+
|
|
108
|
+
| Flag | Description |
|
|
109
|
+
| ------------------------------- | --------------------------------------------------------- |
|
|
110
|
+
| `--today` | Events starting today (default if no filter given) |
|
|
111
|
+
| `--tomorrow` | Events starting tomorrow |
|
|
112
|
+
| `--upcoming 2h` | Events starting within interval (e.g., `2h`, `30m`, `1d`) |
|
|
113
|
+
| `--date 2026-03-09` | Events on a specific date |
|
|
114
|
+
| `--range 2026-03-09 2026-03-11` | Events between two dates (inclusive) |
|
|
115
|
+
|
|
116
|
+
### Output options
|
|
117
|
+
|
|
118
|
+
| Flag | Description |
|
|
119
|
+
| ------------------- | ----------------------------------------------- |
|
|
120
|
+
| `--json` | Output as JSON array (default: formatted table) |
|
|
121
|
+
| `--include-all-day` | Include all-day events (excluded by default) |
|
|
122
|
+
| `--no-attendees` | Omit attendee names from output |
|
|
123
|
+
|
|
124
|
+
### Examples
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
# Concierge: get today + tomorrow for triage
|
|
128
|
+
node scripts/query.mjs --today --tomorrow
|
|
129
|
+
|
|
130
|
+
# Meeting-prep: get upcoming meetings in next 2 hours as JSON
|
|
131
|
+
node scripts/query.mjs --upcoming 2h --json
|
|
132
|
+
|
|
133
|
+
# Chief-of-staff: get this week's events
|
|
134
|
+
node scripts/query.mjs --range 2026-03-09 2026-03-13
|
|
135
|
+
|
|
136
|
+
# Quick count check
|
|
137
|
+
node scripts/query.mjs --today --json | node -e "process.stdin.on('data',d=>console.log(JSON.parse(d).length+' events today'))"
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Constraints
|
|
141
|
+
|
|
142
|
+
- Open database read-only (`readOnly: true`)
|
|
143
|
+
- This sync is stateless — always queries the current sliding window
|
|
144
|
+
- All-day events may have null end times — use start date as end date
|
|
145
|
+
- All-day events have timezone `_float` — omit timezone from output
|