@appchy/jarvis 0.1.35 → 0.1.37

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.
@@ -0,0 +1,111 @@
1
+ import { createRequire as __cr } from 'module'; const require = __cr(import.meta.url);
2
+
3
+ // src/hooks/drop.ts
4
+ import { appendFile, mkdir } from "fs/promises";
5
+ import { join } from "path";
6
+
7
+ // src/config.ts
8
+ import fs from "fs";
9
+ import path from "path";
10
+ import os from "os";
11
+ import { fileURLToPath } from "url";
12
+ var __filename = fileURLToPath(import.meta.url);
13
+ var __dirname = path.dirname(__filename);
14
+ function isDev() {
15
+ const candidates = [
16
+ path.join(__dirname, "env.json"),
17
+ path.join(__dirname, "..", "env.json")
18
+ ];
19
+ for (const envFile of candidates) {
20
+ try {
21
+ const data = JSON.parse(fs.readFileSync(envFile, "utf-8"));
22
+ return data.env === "development";
23
+ } catch {
24
+ }
25
+ }
26
+ return false;
27
+ }
28
+ function resolveConfigDir() {
29
+ if (process.env.JARVIS_CONFIG_DIR) return process.env.JARVIS_CONFIG_DIR;
30
+ const base = path.join(os.homedir(), ".jarvis");
31
+ return isDev() ? path.join(base, "dev") : base;
32
+ }
33
+ function configDir() {
34
+ return resolveConfigDir();
35
+ }
36
+ function getHooksDir() {
37
+ return path.join(configDir(), "hooks");
38
+ }
39
+
40
+ // src/hooks/drop.ts
41
+ async function appendDrop(req) {
42
+ if (!req.sessionId || !req.kind || !req.uniqId) {
43
+ process.stderr.write(
44
+ `[hook] skip ${req.kind ?? "?"} missing field sessionId=${!!req.sessionId} uniqId=${!!req.uniqId}
45
+ `
46
+ );
47
+ return;
48
+ }
49
+ const dir = getHooksDir();
50
+ await mkdir(dir, { recursive: true }).catch((err) => {
51
+ process.stderr.write(`[hook] mkdir failed ${dir}: ${err}
52
+ `);
53
+ });
54
+ const envelope = {
55
+ kind: req.kind,
56
+ sessionId: req.sessionId,
57
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
58
+ uniqId: req.uniqId,
59
+ data: req.data
60
+ };
61
+ const file = join(dir, `${req.sessionId}.jsonl`);
62
+ try {
63
+ await appendFile(file, JSON.stringify(envelope) + "\n");
64
+ process.stderr.write(
65
+ `[hook] ${req.kind} appended sessionId=${req.sessionId} uniqId=${req.uniqId}
66
+ `
67
+ );
68
+ } catch (err) {
69
+ process.stderr.write(`[hook] append failed ${file}: ${err}
70
+ `);
71
+ }
72
+ }
73
+ async function readHookStdin() {
74
+ try {
75
+ const chunks = [];
76
+ for await (const chunk of process.stdin) {
77
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
78
+ }
79
+ const raw = Buffer.concat(chunks).toString("utf-8");
80
+ if (!raw) return null;
81
+ return JSON.parse(raw);
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+ function installHookTimeout(ms = 750) {
87
+ const timer = setTimeout(() => process.exit(0), ms);
88
+ timer.unref();
89
+ }
90
+
91
+ // src/hooks/user-prompt-submit.ts
92
+ installHookTimeout();
93
+ async function main() {
94
+ const p = await readHookStdin();
95
+ if (!p) return;
96
+ const sessionId = p.session_id;
97
+ if (!sessionId) return;
98
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
99
+ const data = {
100
+ ...p.transcript_path ? { transcriptPath: p.transcript_path } : {},
101
+ ...p.cwd ? { cwd: p.cwd } : {}
102
+ };
103
+ await appendDrop({
104
+ kind: "user-prompt-submit",
105
+ sessionId,
106
+ uniqId: `user-prompt-submit-${ts}`,
107
+ data
108
+ });
109
+ }
110
+ void main().finally(() => process.exit(0));
111
+ //# sourceMappingURL=user-prompt-submit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/drop.ts","../../src/config.ts","../../src/hooks/user-prompt-submit.ts"],"sourcesContent":["/**\n * Shared helpers for CC hook scripts.\n *\n * Each hook script (pre-tool-use, session-start, stop, …) is a tiny\n * stdin → append-line → exit binary. They all write to the same per-\n * session JSONL at `<hooksDir>/<sessionId>.jsonl`. This module\n * concentrates the envelope shape and the atomic append so the per-\n * hook scripts stay <30 lines each.\n *\n * Hot-path constraints: NEVER block, NEVER network, NEVER read user\n * input. Pure stdin parse + filesystem append + exit. Worst-case one\n * `mkdir` + one `appendFile` per fire.\n */\n\nimport { appendFile, mkdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport { getHooksDir } from \"../config\";\n\n/** Wire-format envelope written one-per-line to the hook JSONL.\n * The watcher in `sessions.ts` parses this exact shape — keep in sync. */\nexport interface HookDropEnvelope<TData = unknown> {\n kind: string;\n sessionId: string;\n ts: string;\n /** Idempotency key. Lets the watcher's replay-on-restart skip drops\n * whose effects were already emitted (see `HookCursor.applied`). */\n uniqId: string;\n data: TData;\n}\n\nexport interface AppendDropRequest<TData> {\n kind: string;\n sessionId: string;\n uniqId: string;\n data: TData;\n}\n\n/** Append one envelope line to the per-session hook JSONL. Atomic between\n * hook processes via POSIX `O_APPEND` (Node's `fs.appendFile`) — concurrent\n * appends from different CC sessions / hook kinds can't interleave bytes\n * within a line as long as the line stays < `PIPE_BUF` (4KB). Drops are\n * <1KB in practice, so this is safe without locks.\n *\n * Stderr logs are written so `tail -f ~/.jarvis[/dev]/hooks/hook.log` (when\n * CC pipes hook stderr there, which it does by default) shows what fired.\n * Stderr never breaks CC — it only watches stdout for decision JSON. */\nexport async function appendDrop<TData>(req: AppendDropRequest<TData>): Promise<void> {\n if (!req.sessionId || !req.kind || !req.uniqId) {\n process.stderr.write(\n `[hook] skip ${req.kind ?? \"?\"} missing field ` +\n `sessionId=${!!req.sessionId} uniqId=${!!req.uniqId}\\n`,\n );\n return;\n }\n const dir = getHooksDir();\n await mkdir(dir, { recursive: true }).catch((err) => {\n process.stderr.write(`[hook] mkdir failed ${dir}: ${err}\\n`);\n });\n const envelope: HookDropEnvelope<TData> = {\n kind: req.kind,\n sessionId: req.sessionId,\n ts: new Date().toISOString(),\n uniqId: req.uniqId,\n data: req.data,\n };\n const file = join(dir, `${req.sessionId}.jsonl`);\n try {\n await appendFile(file, JSON.stringify(envelope) + \"\\n\");\n process.stderr.write(\n `[hook] ${req.kind} appended sessionId=${req.sessionId} uniqId=${req.uniqId}\\n`,\n );\n } catch (err) {\n process.stderr.write(`[hook] append failed ${file}: ${err}\\n`);\n }\n}\n\n/** Read CC's hook payload from stdin. Returns `null` on parse failure\n * (never throws — the hook must never block CC). */\nexport async function readHookStdin<T = Record<string, unknown>>(): Promise<T | null> {\n try {\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(typeof chunk === \"string\" ? Buffer.from(chunk) : chunk);\n }\n const raw = Buffer.concat(chunks).toString(\"utf-8\");\n if (!raw) return null;\n return JSON.parse(raw) as T;\n } catch {\n return null;\n }\n}\n\n/** Hard cap any hook so an exotic stdin stall can't pile up forever\n * before CC's terminal prompt fires. We exit 0 with no output → CC\n * proceeds with its normal permission flow. */\nexport function installHookTimeout(ms = 750): void {\n const timer = setTimeout(() => process.exit(0), ms);\n timer.unref();\n}\n","/**\n * Agent Config\n *\n * Manages ~/.jarvis/config.json — saved by `jarvis connect <token>`,\n * read on `jarvis start`.\n */\n\nimport fs from \"fs\";\nimport path from \"path\";\nimport os from \"os\";\nimport { fileURLToPath } from \"url\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface AgentConfig {\n /** Cloud WS API URL (from connect token) */\n apiUrl?: string;\n /** JWT auth token (from connect token) */\n token?: string;\n /** Long-lived refresh token for obtaining new access tokens */\n refreshToken?: string;\n /** User ID */\n userId: string;\n /** Environment ID */\n envId?: string;\n /** Workspace root path for repo operations */\n workspacePath?: string;\n /** Web app URL for browser-based auth */\n appUrl?: string;\n /** Anthropic API key (for local-only use without cloud) */\n anthropicApiKey?: string;\n /** Use Anthropic subscription instead of API key */\n useSubscription?: boolean;\n /** When the config was last updated */\n connectedAt?: string;\n}\n\nexport interface ConnectToken {\n apiUrl: string;\n jwt: string;\n refreshToken: string;\n userId: string;\n envId: string;\n}\n\n// =============================================================================\n// Paths\n// =============================================================================\n\n// =============================================================================\n// Environment (single source of truth)\n// =============================================================================\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\n/** Single source of truth for dev vs prod.\n * Two locations, picked by which file is shipped:\n * Dev: apps/cli/env.json (committed, `{ \"env\": \"development\" }`)\n * Prod: apps/cli/dist/env.json (written by tsup build, `{ \"env\": \"production\" }`)\n * We try the prod-style sibling first (next to bin.js when bundled), then fall\n * back to the dev-style parent path (one up from src/config.ts). The prod bundle\n * has no `..` parent env.json, and the dev source tree has no sibling env.json,\n * so the two never collide. Safe default if neither exists: prod. */\nexport function isDev(): boolean {\n const candidates = [\n path.join(__dirname, \"env.json\"),\n path.join(__dirname, \"..\", \"env.json\"),\n ];\n for (const envFile of candidates) {\n try {\n const data = JSON.parse(fs.readFileSync(envFile, \"utf-8\"));\n return data.env === \"development\";\n } catch {\n /* try next candidate */\n }\n }\n return false;\n}\n\nconst PROD_APP_URL = \"https://jarvis.appchy.com\";\nconst DEV_APP_URL = \"http://localhost:3000\";\n\nconst PROD_WS_URL = \"wss://jarvis.appchy.com/ws\";\nconst DEV_WS_URL = \"ws://localhost:7863\";\n\n/** App URL for the current environment. */\nexport function getDefaultAppUrl(): string {\n return isDev() ? (process.env.APP_URL || DEV_APP_URL) : PROD_APP_URL;\n}\n\n/** WS hub URL for the current environment. Hardcoded per build so the prod\n * binary can never subscribe to the dev hub (or vice versa) regardless of\n * what's stored in `config.apiUrl`. Dev permits a `JARVIS_UPSTREAM_URL`\n * override for tests/CI on a non-default port; prod has no escape hatch. */\nexport function getDefaultWsUrl(): string {\n return isDev() ? (process.env.JARVIS_UPSTREAM_URL || DEV_WS_URL) : PROD_WS_URL;\n}\n\n// =============================================================================\n// Config directory\n// =============================================================================\n\n/** Determine config directory.\n * - JARVIS_CONFIG_DIR env var takes priority (escape hatch for CI/testing)\n * - dev → ~/.jarvis/dev/\n * - prod → ~/.jarvis/\n *\n * Called fresh on every read so test environments that set\n * `JARVIS_CONFIG_DIR` AFTER module load still see the override — caching\n * the value at module init would lock tests onto the host's real\n * `~/.jarvis/dev/` and leak production state into the in-memory infra. */\nfunction resolveConfigDir(): string {\n if (process.env.JARVIS_CONFIG_DIR) return process.env.JARVIS_CONFIG_DIR;\n const base = path.join(os.homedir(), \".jarvis\");\n return isDev() ? path.join(base, \"dev\") : base;\n}\n\nfunction configDir(): string {\n return resolveConfigDir();\n}\n\nfunction configFile(): string {\n return path.join(configDir(), \"config.json\");\n}\n\n/** Absolute path to the per-daemon config directory.\n * `prod` → `~/.jarvis/`, `dev` → `~/.jarvis/dev/`. */\nexport function getConfigDir(): string {\n return configDir();\n}\n\n/** Per-daemon directory for PreToolUse hook drop files. The CC hook script\n * writes `<sessionId>/<toolUseId>.json` here; the sessions watcher consumes\n * them. Prod and dev daemons each own their own subtree so a hook installed\n * for one never feeds the other. */\nexport function getHooksDir(): string {\n return path.join(configDir(), \"hooks\");\n}\n\n// =============================================================================\n// Operations\n// =============================================================================\n\nexport function loadConfig(): AgentConfig | null {\n try {\n const raw = fs.readFileSync(configFile(), \"utf-8\");\n return JSON.parse(raw) as AgentConfig;\n } catch {\n return null;\n }\n}\n\nexport function saveConfig(config: AgentConfig): void {\n fs.mkdirSync(configDir(), { recursive: true });\n fs.writeFileSync(configFile(), JSON.stringify(config, null, 2) + \"\\n\");\n}\n\nexport function clearConfig(): void {\n try {\n fs.unlinkSync(configFile());\n } catch {}\n}\n\nexport function parseConnectToken(token: string): ConnectToken {\n try {\n const decoded = Buffer.from(token, \"base64\").toString(\"utf-8\");\n const parsed = JSON.parse(decoded);\n if (!parsed.apiUrl || !parsed.jwt || !parsed.userId) {\n throw new Error(\"Invalid token: missing required fields (apiUrl, jwt, userId)\");\n }\n return parsed as ConnectToken;\n } catch (err) {\n if (err instanceof SyntaxError) {\n throw new Error(\"Invalid token: not valid base64-encoded JSON\");\n }\n throw err;\n }\n}\n\nexport function getConfigPath(): string {\n return configFile();\n}\n","/**\n * UserPromptSubmit hook — drops an envelope each time a terminal `claude`\n * user submits a prompt. The daemon SDK runs with `settingSources: []`\n * so its own `query()` calls never trigger this hook — every drop\n * observed here is unambiguously a terminal-typed turn.\n *\n * Used to flip `SessionCacheEntry.controller` to `\"claudeCode\"` on the\n * watcher side, surfacing the controller change in the web UI's pill.\n */\n\nimport { appendDrop, installHookTimeout, readHookStdin } from \"./drop\";\n\ninterface CcUserPromptSubmitInput {\n session_id?: string;\n transcript_path?: string;\n cwd?: string;\n prompt?: string;\n}\n\ninterface UserPromptSubmitDropData {\n transcriptPath?: string;\n cwd?: string;\n}\n\ninstallHookTimeout();\n\nasync function main(): Promise<void> {\n const p = await readHookStdin<CcUserPromptSubmitInput>();\n if (!p) return;\n const sessionId = p.session_id;\n if (!sessionId) return;\n\n const ts = new Date().toISOString();\n const data: UserPromptSubmitDropData = {\n ...(p.transcript_path ? { transcriptPath: p.transcript_path } : {}),\n ...(p.cwd ? { cwd: p.cwd } : {}),\n };\n\n await appendDrop<UserPromptSubmitDropData>({\n kind: \"user-prompt-submit\",\n sessionId,\n uniqId: `user-prompt-submit-${ts}`,\n data,\n });\n}\n\nvoid main().finally(() => process.exit(0));\n"],"mappings":"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","names":[]}