@appchy/jarvis 0.1.37 → 0.1.39
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/bin/config-change.mjs +1 -1
- package/bin/jarvis.mjs +8 -12
- package/bin/pre-tool-use.mjs +2 -2
- package/bin/session-start.mjs +1 -1
- package/bin/stop.mjs +1 -1
- package/bin/user-prompt-submit.mjs +1 -1
- package/dist/bin.js +10168 -37341
- package/dist/bin.js.map +1 -1
- package/dist/data/backends.mjs +469 -0
- package/dist/data/chunk-7REP35VA.mjs +462 -0
- package/dist/data/chunk-AKQQC5IT.mjs +1224 -0
- package/dist/data/chunk-AYOJSS2F.mjs +72 -0
- package/dist/data/chunk-RRJ6KKYL.mjs +42 -0
- package/dist/data/chunk-YWSWQEJN.mjs +138 -0
- package/dist/data/embedders.mjs +80 -0
- package/dist/data/finders.mjs +35 -0
- package/dist/data/index.mjs +137 -0
- package/dist/data/linkers.mjs +83 -0
- package/dist/data/mcp.mjs +2624 -0
- package/dist/data/persistences.mjs +8 -0
- package/dist/data/rerankers.mjs +52 -0
- package/dist/data/stores.mjs +38 -0
- package/dist/hooks/config-change.js +7 -29
- package/dist/hooks/config-change.js.map +1 -1
- package/dist/hooks/pre-tool-use.js +129 -32
- package/dist/hooks/pre-tool-use.js.map +1 -1
- package/dist/hooks/session-start.js +72 -37
- package/dist/hooks/session-start.js.map +1 -1
- package/dist/hooks/stop.js +140 -32
- package/dist/hooks/stop.js.map +1 -1
- package/dist/hooks/user-prompt-submit.js +7 -29
- package/dist/hooks/user-prompt-submit.js.map +1 -1
- package/harness/assets/architecture-template.md +53 -0
- package/harness/assets/design-template.md +89 -0
- package/harness/assets/domains/architecture.md +43 -0
- package/harness/assets/domains/commercial.md +42 -0
- package/harness/assets/domains/design.md +42 -0
- package/harness/assets/domains/legal.md +43 -0
- package/harness/assets/domains/operations.md +39 -0
- package/harness/assets/domains/product.md +43 -0
- package/harness/assets/domains/quality.md +43 -0
- package/harness/assets/domains/security.md +44 -0
- package/harness/assets/domains/support.md +45 -0
- package/harness/assets/epic-template.md +85 -0
- package/harness/assets/feature-template.md +86 -0
- package/harness/assets/handoff-template.md +26 -0
- package/harness/assets/task-template.md +38 -0
- package/harness/assets/version-template.md +43 -0
- package/harness/harness/__init__.py +7 -0
- package/harness/harness/align.py +582 -0
- package/harness/harness/architecture.py +254 -0
- package/harness/harness/autonomy.py +374 -0
- package/harness/harness/branches.py +408 -0
- package/harness/harness/config.py +1482 -0
- package/harness/harness/coverage.py +199 -0
- package/harness/harness/epic.py +220 -0
- package/harness/harness/events.py +153 -0
- package/harness/harness/extend.py +99 -0
- package/harness/harness/frontmatter.py +218 -0
- package/harness/harness/gate.py +591 -0
- package/harness/harness/generate.py +267 -0
- package/harness/harness/git.py +775 -0
- package/harness/harness/ids.py +140 -0
- package/harness/harness/kickoff.py +231 -0
- package/harness/harness/lint.py +505 -0
- package/harness/harness/model.py +364 -0
- package/harness/harness/peers.py +187 -0
- package/harness/harness/product.py +29 -0
- package/harness/harness/registry.py +382 -0
- package/harness/harness/report.py +227 -0
- package/harness/harness/safety.py +387 -0
- package/harness/harness/scaffold.py +129 -0
- package/harness/harness/shard.py +63 -0
- package/harness/harness/shift.py +348 -0
- package/harness/harness/task.py +507 -0
- package/harness/harness/tree.py +258 -0
- package/harness/harness/version.py +305 -0
- package/harness/harness/wrap.py +217 -0
- package/harness/hooks/guard.py +259 -0
- package/harness/presets/appchy/PRESET.md +717 -0
- package/harness/presets/appchy/references/artifacts.md +539 -0
- package/harness/presets/appchy/references/graph.md +154 -0
- package/harness/presets/appchy/references/operations.md +444 -0
- package/harness/presets/appchy/references/research.md +216 -0
- package/harness/schema/work.config.schema.json +401 -0
- package/harness/test_work.py +5002 -0
- package/harness/work.py +534 -0
- package/package.json +34 -40
- package/bin/config-change.dev.mjs +0 -17
- package/bin/jarvis-dev.mjs +0 -30
- package/bin/pre-tool-use.dev.mjs +0 -23
- package/bin/session-start.dev.mjs +0 -17
- package/bin/stop.dev.mjs +0 -17
- package/bin/user-prompt-submit.dev.mjs +0 -17
- package/dev/bin.js +0 -38821
- package/dev/bin.js.map +0 -1
- package/dev/env.json +0 -1
- package/dev/hooks/config-change.js +0 -110
- package/dev/hooks/config-change.js.map +0 -1
- package/dev/hooks/pre-tool-use.js +0 -120
- package/dev/hooks/pre-tool-use.js.map +0 -1
- package/dev/hooks/session-start.js +0 -115
- package/dev/hooks/session-start.js.map +0 -1
- package/dev/hooks/stop.js +0 -112
- package/dev/hooks/stop.js.map +0 -1
- package/dev/hooks/user-prompt-submit.js +0 -111
- package/dev/hooks/user-prompt-submit.js.map +0 -1
- package/dist/env.json +0 -1
package/dev/env.json
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"env":"development"}
|
|
@@ -1,110 +0,0 @@
|
|
|
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/config-change.ts
|
|
92
|
-
installHookTimeout();
|
|
93
|
-
async function main() {
|
|
94
|
-
const p = await readHookStdin();
|
|
95
|
-
if (!p?.session_id) return;
|
|
96
|
-
const data = {
|
|
97
|
-
...p.permission_mode ? { permissionMode: p.permission_mode } : {},
|
|
98
|
-
...p.source ? { source: p.source } : {},
|
|
99
|
-
...p.file_path ? { filePath: p.file_path } : {}
|
|
100
|
-
};
|
|
101
|
-
await appendDrop({
|
|
102
|
-
kind: "config-change",
|
|
103
|
-
sessionId: p.session_id,
|
|
104
|
-
// ts-based uniqId — ConfigChange can fire repeatedly for the same session.
|
|
105
|
-
uniqId: `config-change-${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
106
|
-
data
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
void main().finally(() => process.exit(0));
|
|
110
|
-
//# sourceMappingURL=config-change.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hooks/drop.ts","../../src/config.ts","../../src/hooks/config-change.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 * ConfigChange hook — drops an envelope when CC's `.claude/settings.json`\n * is edited from outside the session (file write, not slash command).\n *\n * The drop carries the new `permission_mode` so the watcher can emit a\n * `settings_changed` event and the web UI updates without waiting for the\n * next assistant turn. Slash-command mode changes (e.g. `/mode plan`)\n * still propagate via the JSONL `permission-mode` event — this hook only\n * covers settings-file edits.\n */\n\nimport { appendDrop, installHookTimeout, readHookStdin } from \"./drop\";\n\ninterface CcConfigChangeInput {\n session_id?: string;\n permission_mode?: string;\n source?: string;\n file_path?: string;\n}\n\ninterface ConfigChangeDropData {\n permissionMode?: string;\n source?: string;\n filePath?: string;\n}\n\ninstallHookTimeout();\n\nasync function main(): Promise<void> {\n const p = await readHookStdin<CcConfigChangeInput>();\n if (!p?.session_id) return;\n const data: ConfigChangeDropData = {\n ...(p.permission_mode ? { permissionMode: p.permission_mode } : {}),\n ...(p.source ? { source: p.source } : {}),\n ...(p.file_path ? { filePath: p.file_path } : {}),\n };\n await appendDrop<ConfigChangeDropData>({\n kind: \"config-change\",\n sessionId: p.session_id,\n // ts-based uniqId — ConfigChange can fire repeatedly for the same session.\n uniqId: `config-change-${new Date().toISOString()}`,\n data,\n });\n}\n\nvoid main().finally(() => process.exit(0));\n"],"mappings":"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","names":[]}
|
|
@@ -1,120 +0,0 @@
|
|
|
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/pre-tool-use.ts
|
|
92
|
-
installHookTimeout();
|
|
93
|
-
async function main() {
|
|
94
|
-
const p = await readHookStdin();
|
|
95
|
-
if (!p) return;
|
|
96
|
-
const sessionId = p.session_id;
|
|
97
|
-
const toolUseId = p.tool_use_id;
|
|
98
|
-
const toolName = p.tool_name;
|
|
99
|
-
if (!sessionId || !toolUseId || !toolName) return;
|
|
100
|
-
const data = {
|
|
101
|
-
toolUseId,
|
|
102
|
-
toolName,
|
|
103
|
-
toolInput: p.tool_input ?? {},
|
|
104
|
-
...p.transcript_path ? { transcriptPath: p.transcript_path } : {},
|
|
105
|
-
...p.cwd ? { cwd: p.cwd } : {}
|
|
106
|
-
};
|
|
107
|
-
await appendDrop({
|
|
108
|
-
kind: "pre-tool-use",
|
|
109
|
-
sessionId,
|
|
110
|
-
uniqId: toolUseId,
|
|
111
|
-
data
|
|
112
|
-
});
|
|
113
|
-
process.stderr.write(
|
|
114
|
-
`[jarvis] ${toolName} (${toolUseId}) \u2014 also approvable from web/mobile.
|
|
115
|
-
Answering in web will take over this session in-process.
|
|
116
|
-
`
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
void main().finally(() => process.exit(0));
|
|
120
|
-
//# sourceMappingURL=pre-tool-use.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hooks/drop.ts","../../src/config.ts","../../src/hooks/pre-tool-use.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 * PreToolUse hook — appends one envelope line to the per-session\n * hook JSONL and exits.\n *\n * CC invokes this binary before every tool use, before its own\n * permission prompt fires. We drop a tiny envelope (kind, sessionId,\n * uniqId = tool_use_id, data = {toolName, toolInput, toolUseId}) and\n * exit 0 with no output — CC's terminal permission prompt fires as\n * usual and the user can answer there.\n *\n * The daemon's hook-JSONL watcher consumes the line and upserts\n * `interaction.input` onto the matching cached `tool_call` event,\n * giving web/mobile a live gate to resolve. See:\n * apps/cli/src/sessions.ts — tailHookJsonl + applyPreToolUseDrop\n *\n * Hot-path constraints: never block, never RPC, never read user input.\n */\n\nimport { appendDrop, installHookTimeout, readHookStdin } from \"./drop\";\n\ninterface CcPreToolUseInput {\n session_id?: string;\n transcript_path?: string;\n tool_name?: string;\n tool_input?: unknown;\n tool_use_id?: string;\n cwd?: string;\n}\n\ninterface PreToolUseDropData {\n toolUseId: string;\n toolName: string;\n toolInput: unknown;\n transcriptPath?: string;\n cwd?: string;\n}\n\ninstallHookTimeout();\n\nasync function main(): Promise<void> {\n const p = await readHookStdin<CcPreToolUseInput>();\n if (!p) return;\n const sessionId = p.session_id;\n const toolUseId = p.tool_use_id;\n const toolName = p.tool_name;\n if (!sessionId || !toolUseId || !toolName) return;\n\n const data: PreToolUseDropData = {\n toolUseId,\n toolName,\n toolInput: p.tool_input ?? {},\n ...(p.transcript_path ? { transcriptPath: p.transcript_path } : {}),\n ...(p.cwd ? { cwd: p.cwd } : {}),\n };\n\n await appendDrop<PreToolUseDropData>({\n kind: \"pre-tool-use\",\n sessionId,\n uniqId: toolUseId,\n data,\n });\n\n // Inform the user that web/mobile can answer this gate too. CC tees\n // hook stderr to its log; doesn't affect CC's decision flow (stdout\n // is what CC reads for permission JSON, and we don't write stdout).\n // Worth knowing: if you answer in web, the daemon promotes this\n // session to in-process — your terminal `claude` will exit.\n process.stderr.write(\n `[jarvis] ${toolName} (${toolUseId}) — also approvable from web/mobile.\\n` +\n ` Answering in web will take over this session in-process.\\n`,\n );\n}\n\nvoid main().finally(() => process.exit(0));\n"],"mappings":"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","names":[]}
|
|
@@ -1,115 +0,0 @@
|
|
|
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/session-start.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 source = p.source ?? "startup";
|
|
100
|
-
const data = {
|
|
101
|
-
source,
|
|
102
|
-
...p.transcript_path ? { transcriptPath: p.transcript_path } : {},
|
|
103
|
-
...p.cwd ? { cwd: p.cwd } : {}
|
|
104
|
-
};
|
|
105
|
-
await appendDrop({
|
|
106
|
-
kind: "session-start",
|
|
107
|
-
sessionId,
|
|
108
|
-
// ts-based uniqId — SessionStart can fire multiple times per session
|
|
109
|
-
// over its lifetime (e.g. one `startup` then later `resume`).
|
|
110
|
-
uniqId: `session-start-${ts}`,
|
|
111
|
-
data
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
void main().finally(() => process.exit(0));
|
|
115
|
-
//# sourceMappingURL=session-start.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hooks/drop.ts","../../src/config.ts","../../src/hooks/session-start.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 * SessionStart hook — drops an envelope when CC starts/resumes a session.\n *\n * Used by the daemon to surface new external CC sessions in web/mobile\n * before CC's JSONL has been fully parsed — eliminating the chokidar\n * parse latency for fresh sessions.\n */\n\nimport { appendDrop, installHookTimeout, readHookStdin } from \"./drop\";\n\ninterface CcSessionStartInput {\n session_id?: string;\n transcript_path?: string;\n cwd?: string;\n /** \"startup\" (fresh CLI), \"resume\" (--resume <id>), \"clear\" (/clear). */\n source?: string;\n}\n\ninterface SessionStartDropData {\n source: string;\n transcriptPath?: string;\n cwd?: string;\n}\n\ninstallHookTimeout();\n\nasync function main(): Promise<void> {\n const p = await readHookStdin<CcSessionStartInput>();\n if (!p) return;\n const sessionId = p.session_id;\n if (!sessionId) return;\n\n const ts = new Date().toISOString();\n const source = p.source ?? \"startup\";\n const data: SessionStartDropData = {\n source,\n ...(p.transcript_path ? { transcriptPath: p.transcript_path } : {}),\n ...(p.cwd ? { cwd: p.cwd } : {}),\n };\n\n await appendDrop<SessionStartDropData>({\n kind: \"session-start\",\n sessionId,\n // ts-based uniqId — SessionStart can fire multiple times per session\n // over its lifetime (e.g. one `startup` then later `resume`).\n uniqId: `session-start-${ts}`,\n data,\n });\n}\n\nvoid main().finally(() => process.exit(0));\n"],"mappings":"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","names":[]}
|
package/dev/hooks/stop.js
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
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/stop.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
|
-
stopHookActive: !!p.stop_hook_active,
|
|
101
|
-
...p.transcript_path ? { transcriptPath: p.transcript_path } : {},
|
|
102
|
-
...p.cwd ? { cwd: p.cwd } : {}
|
|
103
|
-
};
|
|
104
|
-
await appendDrop({
|
|
105
|
-
kind: "stop",
|
|
106
|
-
sessionId,
|
|
107
|
-
uniqId: `stop-${ts}`,
|
|
108
|
-
data
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
void main().finally(() => process.exit(0));
|
|
112
|
-
//# sourceMappingURL=stop.js.map
|
package/dev/hooks/stop.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hooks/drop.ts","../../src/config.ts","../../src/hooks/stop.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 * Stop hook — drops an envelope when CC's main agent finishes responding.\n *\n * Used by the daemon to flip \"thinking…\" indicators off in web/mobile\n * instantly, instead of waiting for CC's JSONL writes to settle and the\n * watcher's debounced tail to fire.\n */\n\nimport { appendDrop, installHookTimeout, readHookStdin } from \"./drop\";\n\ninterface CcStopInput {\n session_id?: string;\n transcript_path?: string;\n cwd?: string;\n /** True iff this Stop was triggered by a hook (recursion guard). We\n * forward it so the daemon can ignore self-induced stops if needed. */\n stop_hook_active?: boolean;\n}\n\ninterface StopDropData {\n stopHookActive: boolean;\n transcriptPath?: string;\n cwd?: string;\n}\n\ninstallHookTimeout();\n\nasync function main(): Promise<void> {\n const p = await readHookStdin<CcStopInput>();\n if (!p) return;\n const sessionId = p.session_id;\n if (!sessionId) return;\n\n const ts = new Date().toISOString();\n const data: StopDropData = {\n stopHookActive: !!p.stop_hook_active,\n ...(p.transcript_path ? { transcriptPath: p.transcript_path } : {}),\n ...(p.cwd ? { cwd: p.cwd } : {}),\n };\n\n await appendDrop<StopDropData>({\n kind: \"stop\",\n sessionId,\n uniqId: `stop-${ts}`,\n data,\n });\n}\n\nvoid main().finally(() => process.exit(0));\n"],"mappings":"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","names":[]}
|