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.
@@ -1,10 +1,9 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { mkdir, readFile, writeFile } from "node:fs/promises";
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: "auto" });
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 your agent config")
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 agents = parseHookAgents(install.opts().agents);
83
- const command = `${ethosCliCommand()} hooks eager-open`;
84
- const results = [];
85
- for (const agent of agents) {
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 installed for ${results.map((entry) => entry.agent).join(", ")} (command: ${command})`,
91
- data: { command, agents: results },
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 your agent config")
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 agents = parseHookAgents(uninstall.opts().agents);
102
- const results = [];
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 ${results.map((entry) => entry.agent).join(", ")}` : "Nothing to remove.",
110
- data: { agents: results },
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":"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"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ethos-cli",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
4
4
  "description": "Commander-based CLI for Ethos table operations",
5
5
  "homepage": "https://github.com/cluster-software/ethos#readme",
6
6
  "repository": {
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 target = path.join(__dirname, "dist", "scripts", "install-skills.js");
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(target)) {
15
+ if (!fs.existsSync(skillsTarget)) {
13
16
  process.exit(0);
14
17
  }
15
18
 
16
- import(target).catch((err) => {
19
+ function warn(label, err) {
17
20
  process.stderr.write(
18
- "ethos: skills install crashed (continuing) - " +
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
- process.exit(0);
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. The first command in this skill opens the
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 it fails because the CLI is not authenticated, run
29
- `ethos auth login --agent-client auto`, then re-run the `flows start` command.
30
- Do not run a separate `ethos auth status` first; `flows start` already requires
31
- auth and surfaces a clear error when login is needed.
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. Start the flow and open the upload browser in one command (`ethos flows start --open`). Save the `handoff_token` from the response.
38
- 2. Immediately ask the user for ICP, buying signals, person targets, and max contacts while they upload.
39
- 3. Capture the uploaded table id automatically with `ethos flows wait "$HANDOFF_TOKEN" --json`. Do not ask the user to paste the table URL unless `flows wait` cannot recover.
40
- 4. Once the brief is ready, inspect the table, create the `people_sourcing` agent, and create the agent-client-created column with the CLI.
41
- 5. 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.
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.** If the conversation already contains an
64
- injected Ethos handoff for example a `UserPromptSubmit` hook reporting that the
65
- upload browser is already opening and providing a `handoff_token` — do NOT run
66
- `ethos flows start` again. Reuse that `handoff_token`: skip to the brief and then
67
- `ethos flows wait <handoff_token>`. Only run the command below when no such
68
- handoff has been provided.
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
- Start the browser handoff flow through the CLI abstraction instead of constructing client-specific URLs. Pass `--open` so the CLI opens the upload browser itself:
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 --open --json
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. With `--open`, the CLI
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
- With `--open`, the CLI already launched `response.upload_url` in the default
98
- browser and the CSV upload modal opens directly. You usually do not open
99
- anything yourself. Handle the exceptions per client:
100
-
101
- - In Codex, the in-app Browser is the right surface. If `--open` did not surface
102
- the page there, open `response.upload_url` in the Codex Browser. Do not click
103
- `Import CSV` or take other browser actions after navigating unless the user
104
- asks for help.
105
- - In Claude Code, Cursor, or another shell-based client, `--open` uses the OS
106
- opener. If it failed (for example, no desktop session), run `open "$UPLOAD_URL"`
107
- yourself, and only fall back to MCP browser tools, Chrome connectors, or in-app
108
- browser plugins if that also fails. If every option fails, show the URL and ask
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: