ethos-cli 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/hooks.js +21 -109
- package/dist/commands/hooks.js.map +1 -1
- package/dist/lib/hooks-installer.d.ts +23 -0
- package/dist/lib/hooks-installer.js +137 -0
- package/dist/lib/hooks-installer.js.map +1 -0
- package/dist/scripts/install-hooks.d.ts +1 -0
- package/dist/scripts/install-hooks.js +27 -0
- package/dist/scripts/install-hooks.js.map +1 -0
- package/package.json +1 -1
- package/postinstall.cjs +20 -8
- package/skills/find-people-at-companies.md +39 -40
package/dist/commands/hooks.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolveAgentClientId } from "../lib/agent-clients.js";
|
|
5
3
|
import { withClient } from "../lib/command-helpers.js";
|
|
6
4
|
import { isJsonMode } from "../lib/config.js";
|
|
7
5
|
import { createFindPeopleHandoff } from "../lib/find-people-flow.js";
|
|
6
|
+
import { installEagerOpenHook, uninstallEagerOpenHook } from "../lib/hooks-installer.js";
|
|
8
7
|
import { openInBrowser } from "../lib/open-browser.js";
|
|
9
8
|
import { printResult } from "../lib/output.js";
|
|
10
9
|
const SKILL = "find-people-at-companies";
|
|
@@ -12,9 +11,6 @@ const HOOK_EVENT = "UserPromptSubmit";
|
|
|
12
11
|
// Explicit invocation only: the skill named in the prompt, with or without a
|
|
13
12
|
// leading slash. Conservative on purpose so unrelated prompts never open a browser.
|
|
14
13
|
const EAGER_OPEN_TRIGGER = /(^|\s)\/?find-people-at-companies\b/i;
|
|
15
|
-
// Substring used to find/replace/remove our hook entry idempotently.
|
|
16
|
-
const HOOK_COMMAND_MARKER = "hooks eager-open";
|
|
17
|
-
const ALL_HOOK_AGENTS = ["claude-code"];
|
|
18
14
|
export function registerHooks(program) {
|
|
19
15
|
const hooks = program
|
|
20
16
|
.command("hooks")
|
|
@@ -29,14 +25,18 @@ export function matchesEagerOpenTrigger(prompt) {
|
|
|
29
25
|
function registerEagerOpen(hooks) {
|
|
30
26
|
const eagerOpen = hooks
|
|
31
27
|
.command("eager-open")
|
|
32
|
-
.description(`${HOOK_EVENT} hook handler: when the ${SKILL} skill is invoked, pre-open the upload browser and inject the started handoff for the skill to reuse`);
|
|
28
|
+
.description(`${HOOK_EVENT} hook handler (Claude Code): when the ${SKILL} skill is invoked, pre-open the upload browser and inject the started handoff for the skill to reuse`);
|
|
33
29
|
eagerOpen.action(async () => {
|
|
34
30
|
try {
|
|
35
31
|
const prompt = readHookPrompt();
|
|
36
32
|
if (!matchesEagerOpenTrigger(prompt))
|
|
37
33
|
return; // not this skill - leave the prompt untouched
|
|
34
|
+
// Codex's upload is its agent-driven in-app Browser, which this hook can't
|
|
35
|
+
// open; the skill drives Codex, so the hook is a no-op there.
|
|
36
|
+
if (resolveAgentClientId("auto") === "codex")
|
|
37
|
+
return;
|
|
38
38
|
await withClient(eagerOpen, async (client, config) => {
|
|
39
|
-
const handoff = await createFindPeopleHandoff(client, config, { agentClient: "
|
|
39
|
+
const handoff = await createFindPeopleHandoff(client, config, { agentClient: "claude-desktop" });
|
|
40
40
|
openInBrowser(handoff.handoff_url);
|
|
41
41
|
emitAdditionalContext(eagerOpenContext(handoff));
|
|
42
42
|
});
|
|
@@ -76,119 +76,31 @@ function emitAdditionalContext(additionalContext) {
|
|
|
76
76
|
function registerInstall(hooks) {
|
|
77
77
|
const install = hooks
|
|
78
78
|
.command("install")
|
|
79
|
-
.description("Register the find-people eager-open UserPromptSubmit hook in
|
|
80
|
-
.option("-a, --agents <list>", `comma-separated agents (default: claude-code). Supported: ${ALL_HOOK_AGENTS.join(", ")}`);
|
|
79
|
+
.description("Register the find-people eager-open UserPromptSubmit hook in Claude Code (and prune any legacy Codex hook)");
|
|
81
80
|
install.action(async () => {
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (agent === "claude-code")
|
|
87
|
-
results.push(await installClaudeHook(command));
|
|
88
|
-
}
|
|
81
|
+
const { command, claude, codexPruned } = await installEagerOpenHook();
|
|
82
|
+
const notes = [`Claude Code: ${claude.action} (${claude.path})`];
|
|
83
|
+
if (codexPruned.action === "removed")
|
|
84
|
+
notes.push(`removed legacy Codex hook (${codexPruned.path})`);
|
|
89
85
|
printHooksResult(install, {
|
|
90
|
-
label: `Eager-open hook
|
|
91
|
-
data: { command,
|
|
86
|
+
label: `Eager-open hook ${claude.action} for Claude Code (command: ${command})`,
|
|
87
|
+
data: { command, claude, codex_pruned: codexPruned, notes },
|
|
92
88
|
});
|
|
93
89
|
});
|
|
94
90
|
}
|
|
95
91
|
function registerUninstall(hooks) {
|
|
96
92
|
const uninstall = hooks
|
|
97
93
|
.command("uninstall")
|
|
98
|
-
.description("Remove the find-people eager-open hook from
|
|
99
|
-
.option("-a, --agents <list>", `comma-separated agents (default: claude-code). Supported: ${ALL_HOOK_AGENTS.join(", ")}`);
|
|
94
|
+
.description("Remove the find-people eager-open hook from Claude Code (and any legacy Codex hook)");
|
|
100
95
|
uninstall.action(async () => {
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
for (const agent of agents) {
|
|
104
|
-
if (agent === "claude-code")
|
|
105
|
-
results.push(await uninstallClaudeHook());
|
|
106
|
-
}
|
|
107
|
-
const removed = results.reduce((total, entry) => total + entry.removed, 0);
|
|
96
|
+
const { claude, codex } = await uninstallEagerOpenHook();
|
|
97
|
+
const removed = [claude, codex].filter((entry) => entry.action === "removed");
|
|
108
98
|
printHooksResult(uninstall, {
|
|
109
|
-
label: removed ? `Removed the eager-open hook from ${
|
|
110
|
-
data: {
|
|
99
|
+
label: removed.length ? `Removed the eager-open hook from ${removed.map((entry) => entry.path).join(", ")}` : "Nothing to remove.",
|
|
100
|
+
data: { claude, codex },
|
|
111
101
|
});
|
|
112
102
|
});
|
|
113
103
|
}
|
|
114
|
-
async function installClaudeHook(command) {
|
|
115
|
-
const path = claudeSettingsPath();
|
|
116
|
-
const settings = await readClaudeSettings(path);
|
|
117
|
-
const allHooks = (settings.hooks ??= {});
|
|
118
|
-
const list = (allHooks[HOOK_EVENT] ??= []);
|
|
119
|
-
let updated = false;
|
|
120
|
-
for (const group of list) {
|
|
121
|
-
for (const entry of group.hooks ?? []) {
|
|
122
|
-
if (typeof entry.command === "string" && entry.command.includes(HOOK_COMMAND_MARKER)) {
|
|
123
|
-
entry.type = "command";
|
|
124
|
-
entry.command = command;
|
|
125
|
-
updated = true;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
if (!updated) {
|
|
130
|
-
list.push({ hooks: [{ type: "command", command }] });
|
|
131
|
-
}
|
|
132
|
-
await writeJsonFile(path, settings);
|
|
133
|
-
return { agent: "claude-code", path, action: updated ? "updated" : "installed", command };
|
|
134
|
-
}
|
|
135
|
-
async function uninstallClaudeHook() {
|
|
136
|
-
const path = claudeSettingsPath();
|
|
137
|
-
if (!existsSync(path))
|
|
138
|
-
return { agent: "claude-code", path, removed: 0 };
|
|
139
|
-
const settings = await readClaudeSettings(path);
|
|
140
|
-
const list = settings.hooks?.[HOOK_EVENT];
|
|
141
|
-
if (!settings.hooks || !list)
|
|
142
|
-
return { agent: "claude-code", path, removed: 0 };
|
|
143
|
-
let removed = 0;
|
|
144
|
-
const kept = [];
|
|
145
|
-
for (const group of list) {
|
|
146
|
-
const groupHooks = group.hooks ?? [];
|
|
147
|
-
const filtered = groupHooks.filter((entry) => !(typeof entry.command === "string" && entry.command.includes(HOOK_COMMAND_MARKER)));
|
|
148
|
-
removed += groupHooks.length - filtered.length;
|
|
149
|
-
if (filtered.length)
|
|
150
|
-
kept.push({ ...group, hooks: filtered });
|
|
151
|
-
}
|
|
152
|
-
if (kept.length)
|
|
153
|
-
settings.hooks[HOOK_EVENT] = kept;
|
|
154
|
-
else
|
|
155
|
-
delete settings.hooks[HOOK_EVENT];
|
|
156
|
-
await writeJsonFile(path, settings);
|
|
157
|
-
return { agent: "claude-code", path, removed };
|
|
158
|
-
}
|
|
159
|
-
function ethosCliCommand() {
|
|
160
|
-
return process.env.ETHOS_CLI_COMMAND || "ethos";
|
|
161
|
-
}
|
|
162
|
-
function claudeSettingsPath() {
|
|
163
|
-
const base = process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
|
|
164
|
-
return join(base, "settings.json");
|
|
165
|
-
}
|
|
166
|
-
async function readClaudeSettings(path) {
|
|
167
|
-
try {
|
|
168
|
-
const raw = await readFile(path, "utf8");
|
|
169
|
-
const parsed = raw.trim() ? JSON.parse(raw) : {};
|
|
170
|
-
return parsed && typeof parsed === "object" ? parsed : {};
|
|
171
|
-
}
|
|
172
|
-
catch {
|
|
173
|
-
return {};
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
async function writeJsonFile(path, value) {
|
|
177
|
-
await mkdir(dirname(path), { recursive: true });
|
|
178
|
-
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
179
|
-
}
|
|
180
|
-
function parseHookAgents(value) {
|
|
181
|
-
if (!value)
|
|
182
|
-
return ["claude-code"];
|
|
183
|
-
const parsed = [];
|
|
184
|
-
for (const raw of value.split(",").map((part) => part.trim()).filter(Boolean)) {
|
|
185
|
-
if (ALL_HOOK_AGENTS.includes(raw))
|
|
186
|
-
parsed.push(raw);
|
|
187
|
-
else
|
|
188
|
-
throw new Error(`unknown hook agent: ${raw} (supported: ${ALL_HOOK_AGENTS.join(", ")})`);
|
|
189
|
-
}
|
|
190
|
-
return parsed.length ? parsed : ["claude-code"];
|
|
191
|
-
}
|
|
192
104
|
function printHooksResult(command, result) {
|
|
193
105
|
printResult({ apiUrl: "", apiKey: "", json: isJsonMode(command) }, result);
|
|
194
106
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks.js","sourceRoot":"","sources":["../../src/commands/hooks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"hooks.js","sourceRoot":"","sources":["../../src/commands/hooks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AACvD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,uBAAuB,EAA0B,MAAM,4BAA4B,CAAC;AAC7F,OAAO,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AACzF,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE/C,MAAM,KAAK,GAAG,0BAA0B,CAAC;AACzC,MAAM,UAAU,GAAG,kBAAkB,CAAC;AACtC,6EAA6E;AAC7E,oFAAoF;AACpF,MAAM,kBAAkB,GAAG,sCAAsC,CAAC;AAElE,MAAM,UAAU,aAAa,CAAC,OAAgB;IAC5C,MAAM,KAAK,GAAG,OAAO;SAClB,OAAO,CAAC,OAAO,CAAC;SAChB,WAAW,CAAC,oFAAoF,CAAC,CAAC;IAErG,iBAAiB,CAAC,KAAK,CAAC,CAAC;IACzB,eAAe,CAAC,KAAK,CAAC,CAAC;IACvB,iBAAiB,CAAC,KAAK,CAAC,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,MAAc;IACpD,OAAO,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AACzC,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAc;IACvC,MAAM,SAAS,GAAG,KAAK;SACpB,OAAO,CAAC,YAAY,CAAC;SACrB,WAAW,CACV,GAAG,UAAU,yCAAyC,KAAK,sGAAsG,CAClK,CAAC;IACJ,SAAS,CAAC,MAAM,CAAC,KAAK,IAAI,EAAE;QAC1B,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,cAAc,EAAE,CAAC;YAChC,IAAI,CAAC,uBAAuB,CAAC,MAAM,CAAC;gBAAE,OAAO,CAAC,8CAA8C;YAC5F,2EAA2E;YAC3E,8DAA8D;YAC9D,IAAI,oBAAoB,CAAC,MAAM,CAAC,KAAK,OAAO;gBAAE,OAAO;YACrD,MAAM,UAAU,CAAC,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;gBACnD,MAAM,OAAO,GAAG,MAAM,uBAAuB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,WAAW,EAAE,gBAAgB,EAAE,CAAC,CAAC;gBACjG,aAAa,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;gBACnC,qBAAqB,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,CAAC;YACnD,CAAC,CAAC,CAAC;QACL,CAAC;QAAC,MAAM,CAAC;YACP,wFAAwF;QAC1F,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,cAAc;IACrB,IAAI,OAAO,CAAC,KAAK,CAAC,KAAK;QAAE,OAAO,EAAE,CAAC;IACnC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;QACpC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE;YAAE,OAAO,EAAE,CAAC;QAC3B,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAyB,CAAC;QACrD,OAAO,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IAC5D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,SAAS,gBAAgB,CAAC,OAA0B;IAClD,MAAM,SAAS,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,kBAAkB,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IAChF,OAAO,CACL,+DAA+D,SAAS,4BAA4B;QACpG,oCAAoC,KAAK,mDAAmD;QAC5F,iBAAiB,OAAO,CAAC,aAAa,IAAI;QAC1C,uFAAuF;QACvF,sBAAsB,OAAO,CAAC,aAAa,6CAA6C,CACzF,CAAC;AACJ,CAAC;AAED,SAAS,qBAAqB,CAAC,iBAAyB;IACtD,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,IAAI,CAAC,SAAS,CAAC;QACb,kBAAkB,EAAE,EAAE,aAAa,EAAE,UAAU,EAAE,iBAAiB,EAAE;KACrE,CAAC,CACH,CAAC;AACJ,CAAC;AAED,SAAS,eAAe,CAAC,KAAc;IACrC,MAAM,OAAO,GAAG,KAAK;SAClB,OAAO,CAAC,SAAS,CAAC;SAClB,WAAW,CAAC,4GAA4G,CAAC,CAAC;IAC7H,OAAO,CAAC,MAAM,CAAC,KAAK,IAAI,EAAE;QACxB,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,MAAM,oBAAoB,EAAE,CAAC;QACtE,MAAM,KAAK,GAAG,CAAC,gBAAgB,MAAM,CAAC,MAAM,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,CAAC;QACjE,IAAI,WAAW,CAAC,MAAM,KAAK,SAAS;YAAE,KAAK,CAAC,IAAI,CAAC,8BAA8B,WAAW,CAAC,IAAI,GAAG,CAAC,CAAC;QACpG,gBAAgB,CAAC,OAAO,EAAE;YACxB,KAAK,EAAE,mBAAmB,MAAM,CAAC,MAAM,8BAA8B,OAAO,GAAG;YAC/E,IAAI,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,WAAW,EAAE,KAAK,EAAE;SAC5D,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAc;IACvC,MAAM,SAAS,GAAG,KAAK;SACpB,OAAO,CAAC,WAAW,CAAC;SACpB,WAAW,CAAC,qFAAqF,CAAC,CAAC;IACtG,SAAS,CAAC,MAAM,CAAC,KAAK,IAAI,EAAE;QAC1B,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,sBAAsB,EAAE,CAAC;QACzD,MAAM,OAAO,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC;QAC9E,gBAAgB,CAAC,SAAS,EAAE;YAC1B,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,oCAAoC,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,oBAAoB;YAClI,IAAI,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE;SACxB,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,gBAAgB,CAAC,OAAgB,EAAE,MAAwC;IAClF,WAAW,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;AAC7E,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type HookFileResult = {
|
|
2
|
+
path: string;
|
|
3
|
+
action: "installed" | "updated" | "removed" | "absent";
|
|
4
|
+
};
|
|
5
|
+
export declare function claudeCodeDetected(): boolean;
|
|
6
|
+
export declare function claudeSettingsPath(): string;
|
|
7
|
+
/**
|
|
8
|
+
* The command the hook runs. Bound to whichever CLI installs it: the dev CLI
|
|
9
|
+
* sets ETHOS_CLI_COMMAND (so `make dev` points the hook at localhost), while a
|
|
10
|
+
* normal `npm install` / global run has it unset and binds to prod `ethos`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function eagerOpenCommand(): string;
|
|
13
|
+
export declare function installEagerOpenHook(options?: {
|
|
14
|
+
command?: string;
|
|
15
|
+
}): Promise<{
|
|
16
|
+
command: string;
|
|
17
|
+
claude: HookFileResult;
|
|
18
|
+
codexPruned: HookFileResult;
|
|
19
|
+
}>;
|
|
20
|
+
export declare function uninstallEagerOpenHook(): Promise<{
|
|
21
|
+
claude: HookFileResult;
|
|
22
|
+
codex: HookFileResult;
|
|
23
|
+
}>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
// The eager-open hook is Claude Code only. On Codex the CSV upload happens in its
|
|
6
|
+
// agent-driven in-app Browser, which a hook cannot open, so a Codex hook would
|
|
7
|
+
// only create a duplicate handoff — the find-people skill drives Codex itself.
|
|
8
|
+
// ethos-cli <= 0.6.0 also installed a Codex hook; install prunes that legacy file.
|
|
9
|
+
const HOOK_EVENT = "UserPromptSubmit";
|
|
10
|
+
// Substring identifying our handler, so install/uninstall stay idempotent and
|
|
11
|
+
// migrate an older binding (e.g. a dev-CLI path) in place.
|
|
12
|
+
const HOOK_COMMAND_MARKER = "hooks eager-open";
|
|
13
|
+
export function claudeCodeDetected() {
|
|
14
|
+
return existsSync(process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude"));
|
|
15
|
+
}
|
|
16
|
+
export function claudeSettingsPath() {
|
|
17
|
+
return join(process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude"), "settings.json");
|
|
18
|
+
}
|
|
19
|
+
function legacyCodexHooksPath() {
|
|
20
|
+
return join(process.env.CODEX_HOME || join(homedir(), ".codex"), "hooks.json");
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The command the hook runs. Bound to whichever CLI installs it: the dev CLI
|
|
24
|
+
* sets ETHOS_CLI_COMMAND (so `make dev` points the hook at localhost), while a
|
|
25
|
+
* normal `npm install` / global run has it unset and binds to prod `ethos`.
|
|
26
|
+
*/
|
|
27
|
+
export function eagerOpenCommand() {
|
|
28
|
+
return `${process.env.ETHOS_CLI_COMMAND || "ethos"} hooks eager-open`;
|
|
29
|
+
}
|
|
30
|
+
export async function installEagerOpenHook(options = {}) {
|
|
31
|
+
const command = options.command ?? eagerOpenCommand();
|
|
32
|
+
const claude = await mergeHook(claudeSettingsPath(), command);
|
|
33
|
+
// Best-effort: remove any legacy Codex hook so it can't fire a duplicate
|
|
34
|
+
// handoff there. Never let a malformed Codex file fail the Claude install.
|
|
35
|
+
let codexPruned;
|
|
36
|
+
try {
|
|
37
|
+
codexPruned = await removeHook(legacyCodexHooksPath());
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
codexPruned = { path: legacyCodexHooksPath(), action: "absent" };
|
|
41
|
+
}
|
|
42
|
+
return { command, claude, codexPruned };
|
|
43
|
+
}
|
|
44
|
+
export async function uninstallEagerOpenHook() {
|
|
45
|
+
const claude = await removeHook(claudeSettingsPath());
|
|
46
|
+
let codex;
|
|
47
|
+
try {
|
|
48
|
+
codex = await removeHook(legacyCodexHooksPath());
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
codex = { path: legacyCodexHooksPath(), action: "absent" };
|
|
52
|
+
}
|
|
53
|
+
return { claude, codex };
|
|
54
|
+
}
|
|
55
|
+
async function mergeHook(path, command) {
|
|
56
|
+
const config = await readJsonObject(path);
|
|
57
|
+
const allHooks = (config.hooks ??= {});
|
|
58
|
+
const list = (allHooks[HOOK_EVENT] ??= []);
|
|
59
|
+
let updated = false;
|
|
60
|
+
for (const group of list) {
|
|
61
|
+
for (const entry of group.hooks ?? []) {
|
|
62
|
+
if (typeof entry.command === "string" && entry.command.includes(HOOK_COMMAND_MARKER)) {
|
|
63
|
+
entry.type = "command";
|
|
64
|
+
entry.command = command;
|
|
65
|
+
updated = true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (!updated)
|
|
70
|
+
list.push({ hooks: [{ type: "command", command }] });
|
|
71
|
+
await writeJsonFile(path, config);
|
|
72
|
+
return { path, action: updated ? "updated" : "installed" };
|
|
73
|
+
}
|
|
74
|
+
async function removeHook(path) {
|
|
75
|
+
if (!existsSync(path))
|
|
76
|
+
return { path, action: "absent" };
|
|
77
|
+
const config = await readJsonObject(path);
|
|
78
|
+
const list = config.hooks?.[HOOK_EVENT];
|
|
79
|
+
if (!config.hooks || !list)
|
|
80
|
+
return { path, action: "absent" };
|
|
81
|
+
let removed = 0;
|
|
82
|
+
const kept = [];
|
|
83
|
+
for (const group of list) {
|
|
84
|
+
const groupHooks = group.hooks ?? [];
|
|
85
|
+
const filtered = groupHooks.filter((entry) => !(typeof entry.command === "string" && entry.command.includes(HOOK_COMMAND_MARKER)));
|
|
86
|
+
removed += groupHooks.length - filtered.length;
|
|
87
|
+
if (filtered.length)
|
|
88
|
+
kept.push({ ...group, hooks: filtered });
|
|
89
|
+
}
|
|
90
|
+
if (kept.length)
|
|
91
|
+
config.hooks[HOOK_EVENT] = kept;
|
|
92
|
+
else
|
|
93
|
+
delete config.hooks[HOOK_EVENT];
|
|
94
|
+
await writeJsonFile(path, config);
|
|
95
|
+
return { path, action: removed ? "removed" : "absent" };
|
|
96
|
+
}
|
|
97
|
+
async function readJsonObject(path) {
|
|
98
|
+
let raw;
|
|
99
|
+
try {
|
|
100
|
+
raw = await readFile(path, "utf8");
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (error.code === "ENOENT")
|
|
104
|
+
return {}; // no config yet - create it
|
|
105
|
+
throw error; // can't read (e.g. permissions) - don't risk clobbering it
|
|
106
|
+
}
|
|
107
|
+
if (!raw.trim())
|
|
108
|
+
return {};
|
|
109
|
+
// Refuse to parse-and-overwrite a config that exists but is malformed, or the
|
|
110
|
+
// merged write-back would destroy the user's real settings (model, secrets...).
|
|
111
|
+
try {
|
|
112
|
+
const parsed = JSON.parse(raw);
|
|
113
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
114
|
+
throw new Error("expected a JSON object");
|
|
115
|
+
}
|
|
116
|
+
return parsed;
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
throw new Error(`Refusing to modify ${path}: not valid JSON (${error instanceof Error ? error.message : String(error)}). Fix or remove it, then retry.`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async function writeJsonFile(path, value) {
|
|
123
|
+
await mkdir(dirname(path), { recursive: true });
|
|
124
|
+
// Atomic write: a direct writeFile could leave the user's config truncated if
|
|
125
|
+
// interrupted mid-write. Write a sibling temp file, then rename over the target
|
|
126
|
+
// (rename is atomic), so the real file is only ever the old or the complete new.
|
|
127
|
+
const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
128
|
+
await writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`);
|
|
129
|
+
try {
|
|
130
|
+
await rename(tmp, path);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
await rm(tmp, { force: true }).catch(() => { });
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=hooks-installer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks-installer.js","sourceRoot":"","sources":["../../src/lib/hooks-installer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { claudeCodeDetected, installEagerOpenHook } from "../lib/hooks-installer.js";
|
|
2
|
+
// Runs on `npm install`. Re-binds the Claude Code find-people eager-open hook to
|
|
3
|
+
// the CLI being installed — for a normal global install that is prod `ethos`, so
|
|
4
|
+
// installing through npm always returns the hook to production (a `make dev` run
|
|
5
|
+
// rebinds it to the dev CLI for local testing). Also prunes any legacy Codex hook.
|
|
6
|
+
async function main() {
|
|
7
|
+
if (process.env.ETHOS_SKIP_HOOKS || process.env.ETHOS_SKIP_SKILLS) {
|
|
8
|
+
process.stderr.write("ethos: skipping eager-open hook install.\n");
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : null;
|
|
12
|
+
if (uid === 0 && process.env.SUDO_USER) {
|
|
13
|
+
process.stderr.write("ethos: detected sudo install - skipping hook install. Run `ethos hooks install` as your user.\n");
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (!claudeCodeDetected())
|
|
17
|
+
return; // the hook is Claude Code only
|
|
18
|
+
const { command, claude, codexPruned } = await installEagerOpenHook();
|
|
19
|
+
let line = `ethos: eager-open hook ${claude.action} for Claude Code (command: ${command})`;
|
|
20
|
+
if (codexPruned.action === "removed")
|
|
21
|
+
line += "; removed legacy Codex hook";
|
|
22
|
+
process.stderr.write(line + "\n");
|
|
23
|
+
}
|
|
24
|
+
main().catch((err) => {
|
|
25
|
+
process.stderr.write(`ethos: hook install failed (continuing) - ${err instanceof Error ? err.message : String(err)}\nRun \`ethos hooks install\` manually after install completes.\n`);
|
|
26
|
+
});
|
|
27
|
+
//# sourceMappingURL=install-hooks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install-hooks.js","sourceRoot":"","sources":["../../src/scripts/install-hooks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAErF,iFAAiF;AACjF,iFAAiF;AACjF,iFAAiF;AACjF,mFAAmF;AACnF,KAAK,UAAU,IAAI;IACjB,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,OAAO,CAAC,GAAG,CAAC,iBAAiB,EAAE,CAAC;QAClE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,4CAA4C,CAAC,CAAC;QACnE,OAAO;IACT,CAAC;IAED,MAAM,GAAG,GAAG,OAAO,OAAO,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IAC3E,IAAI,GAAG,KAAK,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC;QACvC,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,iGAAiG,CAClG,CAAC;QACF,OAAO;IACT,CAAC;IAED,IAAI,CAAC,kBAAkB,EAAE;QAAE,OAAO,CAAC,+BAA+B;IAElE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,MAAM,oBAAoB,EAAE,CAAC;IACtE,IAAI,IAAI,GAAG,0BAA0B,MAAM,CAAC,MAAM,8BAA8B,OAAO,GAAG,CAAC;IAC3F,IAAI,WAAW,CAAC,MAAM,KAAK,SAAS;QAAE,IAAI,IAAI,6BAA6B,CAAC;IAC5E,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;AACpC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,6CACE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CACjD,mEAAmE,CACpE,CAAC;AACJ,CAAC,CAAC,CAAC"}
|
package/package.json
CHANGED
package/postinstall.cjs
CHANGED
|
@@ -6,18 +6,30 @@
|
|
|
6
6
|
"use strict";
|
|
7
7
|
const fs = require("node:fs");
|
|
8
8
|
const path = require("node:path");
|
|
9
|
+
const { pathToFileURL } = require("node:url");
|
|
9
10
|
|
|
10
|
-
const
|
|
11
|
+
const distScripts = path.join(__dirname, "dist", "scripts");
|
|
12
|
+
const skillsTarget = path.join(distScripts, "install-skills.js");
|
|
13
|
+
const hooksTarget = path.join(distScripts, "install-hooks.js");
|
|
11
14
|
|
|
12
|
-
if (!fs.existsSync(
|
|
15
|
+
if (!fs.existsSync(skillsTarget)) {
|
|
13
16
|
process.exit(0);
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
function warn(label, err) {
|
|
17
20
|
process.stderr.write(
|
|
18
|
-
"ethos:
|
|
19
|
-
(err && err.message ? err.message : String(err)) +
|
|
20
|
-
"\n",
|
|
21
|
+
"ethos: " + label + " crashed (continuing) - " + (err && err.message ? err.message : String(err)) + "\n",
|
|
21
22
|
);
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Each script self-invokes its own async main(); we let the event loop drain
|
|
26
|
+
// naturally rather than force-exiting, so installs finish writing.
|
|
27
|
+
async function bootstrap() {
|
|
28
|
+
// pathToFileURL: dynamic import() of an absolute path needs a file:// URL on Windows.
|
|
29
|
+
await import(pathToFileURL(skillsTarget).href).catch((err) => warn("skills install", err));
|
|
30
|
+
if (fs.existsSync(hooksTarget)) {
|
|
31
|
+
await import(pathToFileURL(hooksTarget).href).catch((err) => warn("hook install", err));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
bootstrap();
|
|
@@ -11,34 +11,28 @@ pass `--api-url` for local development or an explicitly requested environment.
|
|
|
11
11
|
|
|
12
12
|
## Required CLI
|
|
13
13
|
|
|
14
|
-
The Ethos CLI (`ethos`) is required.
|
|
15
|
-
upload browser straight away:
|
|
16
|
-
|
|
17
|
-
```sh
|
|
18
|
-
ethos flows start find-people-at-companies --agent-client auto --open --json
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
If that command fails because `ethos` is not installed or not available on
|
|
14
|
+
The Ethos CLI (`ethos`) is required. If `ethos` is not installed or not on
|
|
22
15
|
`PATH`, install the latest CLI and retry:
|
|
23
16
|
|
|
24
17
|
```sh
|
|
25
18
|
npm install -g ethos-cli@latest --prefer-online
|
|
26
19
|
```
|
|
27
20
|
|
|
28
|
-
If
|
|
29
|
-
`ethos auth login --agent-client auto`, then
|
|
30
|
-
|
|
31
|
-
|
|
21
|
+
If a command fails because the CLI is not authenticated, run
|
|
22
|
+
`ethos auth login --agent-client auto`, then retry. Do not run a separate
|
|
23
|
+
`ethos auth status` first; the flow commands already require auth and surface a
|
|
24
|
+
clear error when login is needed.
|
|
32
25
|
|
|
33
26
|
## Flow
|
|
34
27
|
|
|
35
28
|
Keep CSV upload in the browser-based Ethos product flow:
|
|
36
29
|
|
|
37
|
-
1.
|
|
38
|
-
2.
|
|
39
|
-
3.
|
|
40
|
-
4.
|
|
41
|
-
5.
|
|
30
|
+
1. Get the upload flow. On Claude Code a `UserPromptSubmit` hook may have already started one — if the conversation has an injected `handoff_token`, REUSE it and do NOT run `ethos flows start`. Otherwise run `ethos flows start find-people-at-companies --agent-client auto --json` and save `handoff_token` and `upload_url`. Add `--open` only on shell clients (Claude Code, Cursor) to open the OS browser; on Codex do NOT pass `--open`.
|
|
31
|
+
2. Open `upload_url` in the right surface. On Codex, open it in the in-app Browser — the default surface (OS-open, via `--open` or `open <url>`, is only a fallback if the in-app Browser is not working). On Claude Code and other shell clients, `--open` already opened the OS browser.
|
|
32
|
+
3. Immediately ask the user for ICP, buying signals, person targets, and max contacts while they upload.
|
|
33
|
+
4. Capture the uploaded table id with `ethos flows wait "$HANDOFF_TOKEN" --json`. Do not ask the user to paste the table URL unless `flows wait` cannot recover.
|
|
34
|
+
5. Once the brief is ready, inspect the table, create the `people_sourcing` agent, and create the agent-client-created column with the CLI.
|
|
35
|
+
6. Run the column for the FIRST 3 ROWS ONLY. Tell the user you're running for 3 rows to see if the results are good. If yes, the next step is to run on the whole table. If not, refine the agent based on their feedback so it finds the right people.
|
|
42
36
|
|
|
43
37
|
Do not run `ethos tables import-csv` for this skill unless the user explicitly
|
|
44
38
|
asks for a CLI-only/non-browser fallback. The browser upload is part of the
|
|
@@ -60,19 +54,25 @@ setup.
|
|
|
60
54
|
|
|
61
55
|
## Start Upload Flow
|
|
62
56
|
|
|
63
|
-
**Reuse a pre-started flow first
|
|
64
|
-
|
|
65
|
-
upload browser is already opening
|
|
66
|
-
`ethos flows start` again
|
|
67
|
-
`ethos flows wait <handoff_token>`.
|
|
68
|
-
|
|
57
|
+
**Reuse a pre-started flow first (Claude Code).** On Claude Code a
|
|
58
|
+
`UserPromptSubmit` hook may have already started the flow and injected a
|
|
59
|
+
`handoff_token` ("upload browser is already opening"). If so, do NOT run
|
|
60
|
+
`ethos flows start` again — reuse that `handoff_token`: skip to the brief and
|
|
61
|
+
then `ethos flows wait <handoff_token>`. The hook does not run on Codex, so there
|
|
62
|
+
you always run the command below.
|
|
69
63
|
|
|
70
|
-
|
|
64
|
+
Otherwise start the handoff through the CLI abstraction instead of constructing
|
|
65
|
+
client-specific URLs:
|
|
71
66
|
|
|
72
67
|
```sh
|
|
73
|
-
ethos flows start find-people-at-companies --agent-client auto --
|
|
68
|
+
ethos flows start find-people-at-companies --agent-client auto --json
|
|
74
69
|
```
|
|
75
70
|
|
|
71
|
+
Add `--open` only on shell clients (Claude Code, Cursor) so the CLI opens the OS
|
|
72
|
+
browser. On Codex, do NOT pass `--open` — open `upload_url` in the in-app Browser
|
|
73
|
+
(see Open Browser Upload); use `--open` there only as a fallback if the in-app
|
|
74
|
+
Browser is not working.
|
|
75
|
+
|
|
76
76
|
Save these fields from `response`:
|
|
77
77
|
|
|
78
78
|
- `handoff_token` (needed for `ethos flows wait`)
|
|
@@ -83,8 +83,7 @@ Save these fields from `response`:
|
|
|
83
83
|
- `next_prompt`
|
|
84
84
|
|
|
85
85
|
The `upload_url` is a handoff URL that verifies the browser session and
|
|
86
|
-
workspace before redirecting to the CSV upload modal.
|
|
87
|
-
already launched it in the default browser.
|
|
86
|
+
workspace before redirecting to the CSV upload modal.
|
|
88
87
|
|
|
89
88
|
If the detected client is wrong, rerun the same command with an explicit client,
|
|
90
89
|
such as `--agent-client codex` or `--agent-client claude-desktop`.
|
|
@@ -94,19 +93,19 @@ session.
|
|
|
94
93
|
|
|
95
94
|
## Open Browser Upload
|
|
96
95
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
`Import CSV` or take other browser actions after navigating unless
|
|
104
|
-
asks for help.
|
|
105
|
-
-
|
|
106
|
-
|
|
107
|
-
yourself, and only fall back to MCP browser tools, Chrome
|
|
108
|
-
browser plugins if that also fails. If every option
|
|
109
|
-
the user to open it.
|
|
96
|
+
Open the upload page in the browser surface for this client. Use `upload_url`
|
|
97
|
+
from the reused hook handoff (Claude Code) or from the `flows start` response.
|
|
98
|
+
|
|
99
|
+
- **Codex**: open `upload_url` in the **in-app Browser** — the default and only
|
|
100
|
+
surface you open. Do NOT also OS-open it. Only if the user says the in-app
|
|
101
|
+
Browser is not working, fall back to OS-opening it (`open "$UPLOAD_URL"`). Do
|
|
102
|
+
not click `Import CSV` or take other browser actions after navigating unless
|
|
103
|
+
the user asks for help.
|
|
104
|
+
- **Claude Code, Cursor, or another shell-based client**: `--open` already
|
|
105
|
+
opened the OS browser. If it failed (for example, no desktop session), run
|
|
106
|
+
`open "$UPLOAD_URL"` yourself, and only fall back to MCP browser tools, Chrome
|
|
107
|
+
connectors, or in-app browser plugins if that also fails. If every option
|
|
108
|
+
fails, show the URL and ask the user to open it.
|
|
110
109
|
|
|
111
110
|
Right after the browser opens, immediately ask `response.next_prompt`. If the
|
|
112
111
|
flow response is not available for some reason, ask:
|