New:Socket for Asana Is Now Available.Learn more
Get Started

ashby-cli

Package Overview
Dependencies
Maintainers
1
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

ashby-cli - npm Package Compare versions

Comparing version
0.1.2
to
0.1.3
+124
dist/feed.js
function firstTimestamp(...values) {
for (const value of values) {
if (typeof value === "string" && value.trim())
return value;
}
return null;
}
function safeString(value) {
return typeof value === "string" && value.trim() ? value : undefined;
}
function mapHistoryItem(item) {
const at = firstTimestamp(item?.enteredStageAt, item?.createdAt, item?.updatedAt);
if (!at)
return null;
return {
kind: "stage_changed",
at,
title: item?.title ? `Entered stage: ${item.title}` : "Entered stage",
detail: safeString(item?.stageId),
sourceId: safeString(item?.id),
raw: item,
};
}
function mapNoteItem(item) {
const at = firstTimestamp(item?.createdAt, item?.updatedAt);
if (!at)
return null;
const author = [safeString(item?.author?.firstName), safeString(item?.author?.lastName)].filter(Boolean).join(" ").trim() ||
safeString(item?.author?.email);
return {
kind: "note_added",
at,
title: author ? `Note by ${author}` : "Note added",
detail: safeString(item?.note),
sourceId: safeString(item?.id),
raw: item,
};
}
function mapFeedbackItem(item) {
const at = firstTimestamp(item?.submittedAt, item?.createdAt, item?.updatedAt);
if (!at)
return null;
const author = [safeString(item?.author?.firstName), safeString(item?.author?.lastName)].filter(Boolean).join(" ").trim() ||
safeString(item?.author?.email);
return {
kind: "feedback_submitted",
at,
title: author ? `Feedback from ${author}` : "Feedback submitted",
detail: safeString(item?.recommendation) || safeString(item?.feedbackFormDefinition?.title),
sourceId: safeString(item?.id),
raw: item,
};
}
function mapScheduleItem(item) {
const at = firstTimestamp(item?.updatedAt, item?.createdAt);
if (!at)
return null;
return {
kind: "interview_schedule",
at,
title: item?.status ? `Interview schedule: ${item.status}` : "Interview scheduled",
detail: safeString(item?.interviewStageId),
sourceId: safeString(item?.id),
raw: item,
};
}
function mapInterviewEventItem(item) {
const at = firstTimestamp(item?.startTime, item?.updatedAt, item?.createdAt);
if (!at)
return null;
const interviewerNames = Array.isArray(item?.interviewers)
? item.interviewers
.map((interviewer) => [safeString(interviewer?.firstName), safeString(interviewer?.lastName)].filter(Boolean).join(" ").trim())
.filter(Boolean)
.join(", ")
: "";
return {
kind: "interview_event",
at,
title: interviewerNames ? `Interview event with ${interviewerNames}` : "Interview event",
detail: safeString(item?.meetingLink) || safeString(item?.location),
sourceId: safeString(item?.id),
raw: item,
};
}
export function buildApplicationFeed(input) {
const scheduleItems = (input.schedules || []).flatMap((schedule) => {
const mapped = [];
const scheduleItem = mapScheduleItem(schedule);
if (scheduleItem)
mapped.push(scheduleItem);
if (Array.isArray(schedule?.interviewEvents)) {
for (const interviewEvent of schedule.interviewEvents) {
const eventItem = mapInterviewEventItem(interviewEvent);
if (eventItem)
mapped.push(eventItem);
}
}
return mapped;
});
const explicitEventItems = (input.interviewEvents || [])
.map((item) => mapInterviewEventItem(item))
.filter((item) => Boolean(item));
const all = [
...(input.history || []).map((item) => mapHistoryItem(item)),
...(input.notes || []).map((item) => mapNoteItem(item)),
...(input.feedback || []).map((item) => mapFeedbackItem(item)),
...scheduleItems,
...explicitEventItems,
].filter((item) => Boolean(item));
const seen = new Set();
const deduped = [];
for (const item of all) {
const key = `${item.kind}:${item.sourceId || ""}:${item.at}`;
if (seen.has(key))
continue;
seen.add(key);
deduped.push(item);
}
return deduped.sort((a, b) => (a.at < b.at ? 1 : a.at > b.at ? -1 : 0));
}
export function formatFeedItem(item) {
return `${item.at}\t${item.kind}\t${item.title}${item.detail ? `\t${item.detail}` : ""}`;
}
+18
-0

@@ -74,2 +74,8 @@ const BASE_URL = "https://api.ashbyhq.com";

}
async applicationListHistory(applicationId) {
return this.request("application.listHistory", { applicationId });
}
async applicationFeedbackList(applicationId) {
return this.request("applicationFeedback.list", { applicationId });
}
async applicationCreate(input) {

@@ -84,2 +90,14 @@ return this.request("application.create", input);

}
async candidateListNotes(candidateId, cursor) {
return this.request("candidate.listNotes", cursor ? { candidateId, cursor } : { candidateId });
}
async interviewScheduleList(input = {}) {
return this.request("interviewSchedule.list", input);
}
async interviewEventList(input = {}) {
return this.request("interviewEvent.list", input);
}
async interviewInfo(interviewId) {
return this.request("interview.info", { interviewId });
}
}

@@ -11,2 +11,3 @@ #!/usr/bin/env node

import { formatCandidateRow, validateCandidateSearchInput } from "./candidates.js";
import { buildApplicationFeed, formatFeedItem } from "./feed.js";
import { fail, makeError, ok, printJson } from "./output.js";

@@ -60,2 +61,10 @@ function getCliVersion() {

}
function printJsonHuman(value) {
console.log(JSON.stringify(value, null, 2));
}
function printFeedHuman(items) {
for (const item of items) {
console.log(formatFeedItem(item));
}
}
async function runAction(opts, action, human, meta) {

@@ -305,2 +314,22 @@ try {

candidate
.command("notes")
.description("List notes for a candidate")
.requiredOption("--candidate-id <candidate-id>", "Candidate id")
.option("--cursor <cursor>", "Pagination cursor")
.option("--json", "Emit JSON output")
.action(async (opts) => {
const apiKey = await requireApiKey(opts);
if (!apiKey)
return;
await runAction(opts, async () => {
const response = await createClient(apiKey).candidateListNotes(opts.candidateId, opts.cursor);
return {
count: (response.results || []).length,
items: response.results || [],
nextCursor: response.nextCursor,
moreDataAvailable: response.moreDataAvailable || false,
};
}, (value) => printJsonHuman(value.items));
});
candidate
.command("create")

@@ -382,2 +411,74 @@ .description("Create a candidate")

application
.command("history")
.description("List application stage/history entries")
.requiredOption("--application-id <application-id>", "Application id")
.option("--json", "Emit JSON output")
.action(async (opts) => {
const apiKey = await requireApiKey(opts);
if (!apiKey)
return;
await runAction(opts, async () => {
const response = await createClient(apiKey).applicationListHistory(opts.applicationId);
return {
count: (response.results || []).length,
items: response.results || [],
moreDataAvailable: response.moreDataAvailable || false,
nextCursor: response.nextCursor,
};
}, (value) => printJsonHuman(value.items));
});
application
.command("feedback")
.description("List application feedback")
.requiredOption("--application-id <application-id>", "Application id")
.option("--json", "Emit JSON output")
.action(async (opts) => {
const apiKey = await requireApiKey(opts);
if (!apiKey)
return;
await runAction(opts, async () => {
const response = await createClient(apiKey).applicationFeedbackList(opts.applicationId);
return {
count: (response.results || []).length,
items: response.results || [],
moreDataAvailable: response.moreDataAvailable || false,
nextCursor: response.nextCursor,
};
}, (value) => printJsonHuman(value.items));
});
application
.command("feed")
.description("Build a synthetic application feed from public API surfaces")
.requiredOption("--application-id <application-id>", "Application id")
.option("--no-notes", "Do not include candidate notes")
.option("--json", "Emit JSON output")
.action(async (opts) => {
const apiKey = await requireApiKey(opts);
if (!apiKey)
return;
await runAction(opts, async () => {
const client = createClient(apiKey);
const app = await client.applicationInfo(opts.applicationId);
const candidateId = app.results?.candidate?.id;
const [historyResponse, feedbackResponse, scheduleResponse] = await Promise.all([
client.applicationListHistory(opts.applicationId),
client.applicationFeedbackList(opts.applicationId),
client.interviewScheduleList({ applicationId: opts.applicationId }),
]);
const notesResponse = opts.notes === false || !candidateId ? { results: [], moreDataAvailable: false } : await client.candidateListNotes(candidateId);
const feed = buildApplicationFeed({
history: historyResponse.results || app.results?.applicationHistory || [],
notes: notesResponse.results || [],
feedback: feedbackResponse.results || [],
schedules: scheduleResponse.results || [],
});
return {
applicationId: opts.applicationId,
candidateId: candidateId || null,
count: feed.length,
items: feed,
};
}, (value) => printFeedHuman(value.items));
});
application
.command("create")

@@ -427,2 +528,3 @@ .description("Create an application for a candidate")

const stage = program.command("stage").description("Interview stage metadata");
const interview = program.command("interview").description("Interview schedule and event operations");
stage

@@ -442,2 +544,56 @@ .command("list")

});
interview
.command("schedules")
.description("List interview schedules")
.option("--application-id <application-id>", "Filter by application id")
.option("--interview-schedule-id <interview-schedule-id>", "Filter by interview schedule id")
.option("--interview-id <interview-id>", "Filter by interview id")
.option("--cursor <cursor>", "Pagination cursor")
.option("--json", "Emit JSON output")
.action(async (opts) => {
const apiKey = await requireApiKey(opts);
if (!apiKey)
return;
await runAction(opts, async () => {
const response = await createClient(apiKey).interviewScheduleList({
applicationId: opts.applicationId,
interviewScheduleId: opts.interviewScheduleId,
interviewId: opts.interviewId,
cursor: opts.cursor,
});
return {
count: (response.results || []).length,
items: response.results || [],
moreDataAvailable: response.moreDataAvailable || false,
nextCursor: response.nextCursor,
};
}, (value) => printJsonHuman(value.items));
});
interview
.command("events")
.description("List interview events")
.option("--application-id <application-id>", "Filter by application id")
.option("--interview-schedule-id <interview-schedule-id>", "Filter by interview schedule id")
.option("--interview-id <interview-id>", "Filter by interview id")
.option("--cursor <cursor>", "Pagination cursor")
.option("--json", "Emit JSON output")
.action(async (opts) => {
const apiKey = await requireApiKey(opts);
if (!apiKey)
return;
await runAction(opts, async () => {
const response = await createClient(apiKey).interviewEventList({
applicationId: opts.applicationId,
interviewScheduleId: opts.interviewScheduleId,
interviewId: opts.interviewId,
cursor: opts.cursor,
});
return {
count: (response.results || []).length,
items: response.results || [],
moreDataAvailable: response.moreDataAvailable || false,
nextCursor: response.nextCursor,
};
}, (value) => printJsonHuman(value.items));
});
program.parseAsync(process.argv);
+1
-1
{
"name": "ashby-cli",
"version": "0.1.2",
"version": "0.1.3",
"description": "Agent-first CLI for Ashby's official API",

@@ -5,0 +5,0 @@ "license": "MIT",

@@ -10,2 +10,5 @@ # ashby-cli

- candidate notes
- application history and feedback
- interview schedules
- synthetic feed reconstruction from public API surfaces
- hiring pipeline state

@@ -103,2 +106,3 @@

ashby candidate get <candidate-id> --json
ashby candidate notes --candidate-id <candidate-id> --json
ashby candidate create --name "Jane Doe" --email "jane@example.com" --linkedin-url "https://linkedin.com/in/jane" --json

@@ -113,2 +117,5 @@ ashby note create --candidate-id <candidate-id> --note "Strong fast-track candidate" --json

ashby application get <application-id> --json
ashby application history --application-id <application-id> --json
ashby application feedback --application-id <application-id> --json
ashby application feed --application-id <application-id> --json
ashby application create --candidate-id <candidate-id> --job-id <job-id> --interview-stage-id <stage-id> --json

@@ -122,2 +129,4 @@ ashby application stage-change --application-id <application-id> --interview-stage-id <stage-id> --json

ashby stage list --interview-plan-id <plan-id> --json
ashby interview schedules --application-id <application-id> --json
ashby interview events --application-id <application-id> --json
```

@@ -138,2 +147,18 @@

## Feed coverage
`ashby application feed` reconstructs a useful candidate/application timeline from public API data:
- application history
- candidate notes
- feedback
- interview schedules
- nested interview events
It does **not** provide full parity with the Ashby web UI feed. In particular, public API coverage still appears weak or absent for:
- synced/sent email thread history
- text thread history
- the exact fully merged UI feed object
See `docs/CONTRACT_V1.md` for the stable CLI contract.