pasika 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md ADDED
@@ -0,0 +1,15 @@
1
+ # pasika
2
+ pasika scope and usage.
3
+ Full documentation: docs/common/overview.md
4
+
5
+ ---
6
+
7
+ # Merge Behavior
8
+ How pasika merges into existing Claude settings.
9
+ Full documentation: docs/common/merge-behavior.md
10
+
11
+ ---
12
+
13
+ # Claude Hooks
14
+ Shared Claude hooks shipped by pasika.
15
+ Full documentation: docs/claude/hooks.md
package/CLAUDE.md ADDED
@@ -0,0 +1 @@
1
+ @AGENTS.md
package/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # pasika
2
+
3
+ Reusable base assets for Claude Code setups.
4
+
5
+ `pasika` stores portable Claude base config assets that individual repos can copy from or adapt.
6
+
7
+ ## Scope
8
+
9
+ This repo intentionally starts narrow:
10
+
11
+ - Claude only for v1
12
+ - reusable base config only
13
+ - shared docs at the repo root
14
+ - no project-specific rules, workflows, or business logic
15
+
16
+ ## Layout
17
+
18
+ ```text
19
+ claude/
20
+ scripts/
21
+ render-settings.ts
22
+ .claude/
23
+ settings.base.json
24
+ hooks/
25
+ status-line/
26
+ index.js
27
+ notification.sh
28
+ protect-files.sh
29
+ docs/
30
+ common/
31
+ overview.md
32
+ merge-behavior.md
33
+ claude/
34
+ hooks.md
35
+ scripts/
36
+ pasika.ts
37
+ dist/
38
+ ...
39
+ vulyk.json
40
+ AGENTS.md
41
+ CLAUDE.md
42
+ ```
43
+
44
+ ## What belongs here
45
+
46
+ - portable hook scripts
47
+ - base settings templates
48
+ - shared docs and generated `CLAUDE.md`
49
+ - shared naming and layout conventions
50
+
51
+ ## What stays in project repos
52
+
53
+ - final `.claude` folders
54
+ - project-specific skills, rules, agents, and prompts
55
+ - repository-specific plugin choices
56
+ - scripts that depend on a specific app, CI setup, or codebase
57
+
58
+ ## CLI
59
+
60
+ The main entry point is:
61
+
62
+ ```bash
63
+ npx pasika claude
64
+ ```
65
+
66
+ Optional flags:
67
+
68
+ - `--target-dir <path>` writes into another repo
69
+ - `--force` replaces an existing generated file instead of merging
70
+
71
+ ## Development
72
+
73
+ The CLI source lives in TypeScript.
74
+
75
+ ```bash
76
+ npm run lint
77
+ npm run typecheck
78
+ npm run build
79
+ ```
80
+
81
+ `npm run build` uses `vulyk docs` to emit root `AGENTS.md` and `CLAUDE.md`.
82
+
83
+ ## Recommended Integration
84
+
85
+ Hooks and helper executables should come from `node_modules`.
86
+
87
+ That gives us:
88
+
89
+ - versioned reusable scripts
90
+ - easy upgrades through the package manager
91
+ - no manual copying of hook files into every repo
92
+
93
+ `settings.json` is different. It still needs to exist in each project repo, because Claude Code does not give us a clean inheritance model for it.
94
+
95
+ So the recommended pattern is:
96
+
97
+ 1. install `pasika` as a dev dependency
98
+ 2. run `npx pasika claude`
99
+ 3. generate or merge into `.claude/settings.json` in the project
100
+ 4. point hook commands at `./node_modules/pasika/claude/...`
101
+ 5. keep project-specific plugin, skill, and rule decisions in the project repo
102
+
103
+ ## Merge Behavior
104
+
105
+ By default, `pasika` merges its Claude base into an existing `.claude/settings.json`.
106
+
107
+ That means:
108
+
109
+ - it updates the portable base pieces shipped by `pasika`
110
+ - it preserves unrelated project-specific settings such as extra hooks, plugin config, and custom permissions
111
+
112
+ Use `--force` only when you want to replace the existing file with the `pasika` base output.
@@ -0,0 +1,38 @@
1
+ #!/bin/bash
2
+ # notification.sh - Cross-platform notification
3
+
4
+ OS="$(uname -s)"
5
+
6
+ case "$OS" in
7
+ Darwin*)
8
+ if command -v terminal-notifier &> /dev/null; then
9
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
10
+ TEMP_SCRIPT=$(mktemp)
11
+
12
+ cat > "$TEMP_SCRIPT" <<SCRIPTEND
13
+ #!/bin/bash
14
+ /usr/local/bin/code -r "$PROJECT_DIR"
15
+ sleep 0.5
16
+ osascript -e 'tell application "Visual Studio Code" to activate'
17
+ SCRIPTEND
18
+
19
+ chmod +x "$TEMP_SCRIPT"
20
+
21
+ terminal-notifier \
22
+ -message "Claude Code needs your attention - Click to focus terminal" \
23
+ -title "Claude Code" \
24
+ -sound Ping \
25
+ -execute "$TEMP_SCRIPT"
26
+
27
+ (sleep 60; rm -f "$TEMP_SCRIPT") &
28
+ else
29
+ osascript -e 'display notification "Claude Code needs your attention" with title "Claude Code"'
30
+ fi
31
+ ;;
32
+ Linux*)
33
+ notify-send 'Claude Code' 'Claude Code needs your attention'
34
+ ;;
35
+ MINGW*|MSYS*|CYGWIN*)
36
+ powershell.exe -Command "[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('Claude Code needs your attention', 'Claude Code')"
37
+ ;;
38
+ esac
@@ -0,0 +1,21 @@
1
+ #!/bin/bash
2
+ # protect-files.sh - Block edits to sensitive files
3
+
4
+ INPUT=$(cat)
5
+ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
6
+
7
+ PROTECTED_PATTERNS=(
8
+ "/\.env$|^\.env$"
9
+ "package-lock\.json$"
10
+ "/\.git/|^\.git/"
11
+ )
12
+
13
+ for pattern in "${PROTECTED_PATTERNS[@]}"; do
14
+ if echo "$FILE_PATH" | grep -qE "$pattern"; then
15
+ echo "Blocked: Cannot edit protected file '$FILE_PATH'" >&2
16
+ echo "Reason: This is a sensitive/generated file that should not be manually edited" >&2
17
+ exit 2
18
+ fi
19
+ done
20
+
21
+ exit 0
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execSync } from "node:child_process";
4
+ import path from "node:path";
5
+
6
+ let input = "";
7
+
8
+ process.stdin.on("data", (chunk) => {
9
+ input += chunk;
10
+ });
11
+
12
+ process.stdin.on("end", () => {
13
+ try {
14
+ const data = JSON.parse(input || "{}");
15
+ const model = data.model?.display_name || data.model?.name || "Claude";
16
+ const dir = path.basename(data.workspace?.current_dir || process.cwd());
17
+ const cost = Number(data.cost?.total_cost_usd || 0);
18
+ const pct = Math.floor(Number(data.context_window?.used_percentage || 0));
19
+ const durationMs = Number(data.cost?.total_duration_ms || 0);
20
+
21
+ const CYAN = "\x1b[36m";
22
+ const GREEN = "\x1b[32m";
23
+ const YELLOW = "\x1b[33m";
24
+ const RED = "\x1b[31m";
25
+ const RESET = "\x1b[0m";
26
+
27
+ let barColor = GREEN;
28
+ if (pct >= 90) {
29
+ barColor = RED;
30
+ } else if (pct >= 70) {
31
+ barColor = YELLOW;
32
+ }
33
+
34
+ const filled = Math.max(0, Math.min(10, Math.floor(pct / 10)));
35
+ const bar = "█".repeat(filled) + "░".repeat(10 - filled);
36
+ const mins = Math.floor(durationMs / 60000);
37
+ const secs = Math.floor((durationMs % 60000) / 1000);
38
+
39
+ let branch = "";
40
+ try {
41
+ branch = execSync("git branch --show-current", {
42
+ encoding: "utf8",
43
+ stdio: ["pipe", "pipe", "ignore"],
44
+ }).trim();
45
+ branch = branch ? ` | 🌿 ${branch}` : "";
46
+ } catch {
47
+ branch = "";
48
+ }
49
+
50
+ process.stdout.write(`${CYAN}[${model}]${RESET} 📁 ${dir}${branch}\n`);
51
+ process.stdout.write(
52
+ `${barColor}${bar}${RESET} ${pct}% | ${YELLOW}$${cost.toFixed(2)}${RESET} | ⏱️ ${mins}m ${secs}s\n`,
53
+ );
54
+ } catch {
55
+ process.stdout.write("Claude\n");
56
+ }
57
+ });
@@ -0,0 +1,38 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/claude-code-settings.json",
3
+ "attribution": {
4
+ "commit": "",
5
+ "pr": ""
6
+ },
7
+ "permissions": {
8
+ "ask": ["Bash(git commit:*)", "Bash(git push:*)"]
9
+ },
10
+ "hooks": {
11
+ "Notification": [
12
+ {
13
+ "matcher": "",
14
+ "hooks": [
15
+ {
16
+ "type": "command",
17
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/notification.sh"
18
+ }
19
+ ]
20
+ }
21
+ ],
22
+ "PreToolUse": [
23
+ {
24
+ "matcher": "Edit|Write",
25
+ "hooks": [
26
+ {
27
+ "type": "command",
28
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/protect-files.sh"
29
+ }
30
+ ]
31
+ }
32
+ ]
33
+ },
34
+ "statusLine": {
35
+ "type": "command",
36
+ "command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/status-line/index.js"
37
+ }
38
+ }
@@ -0,0 +1,207 @@
1
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { constants } from "node:fs";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+
9
+ interface ClaudeSettings {
10
+ $schema?: string;
11
+ attribution?: Record<string, unknown>;
12
+ permissions?: {
13
+ ask?: string[];
14
+ deny?: string[];
15
+ };
16
+ hooks: {
17
+ Notification?: HookMatcherEntry[];
18
+ PreToolUse?: HookMatcherEntry[];
19
+ [key: string]: unknown;
20
+ };
21
+ statusLine: {
22
+ type?: string;
23
+ command: string;
24
+ };
25
+ [key: string]: unknown;
26
+ }
27
+
28
+ interface CommandHook {
29
+ type?: string;
30
+ command: string;
31
+ [key: string]: unknown;
32
+ }
33
+
34
+ interface HookMatcherEntry {
35
+ matcher?: string;
36
+ hooks: CommandHook[];
37
+ [key: string]: unknown;
38
+ }
39
+
40
+ const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null;
41
+
42
+ const fileExists = async (filePath: string): Promise<boolean> => {
43
+ try {
44
+ await access(filePath, constants.F_OK);
45
+ return true;
46
+ } catch {
47
+ return false;
48
+ }
49
+ };
50
+
51
+ const isClaudeSettings = (value: unknown): value is ClaudeSettings => {
52
+ if (!isRecord(value)) {
53
+ return false;
54
+ }
55
+
56
+ const hooks = value.hooks;
57
+ const statusLine = value.statusLine;
58
+
59
+ if (!isRecord(hooks) || !isRecord(statusLine) || typeof statusLine.command !== "string") {
60
+ return false;
61
+ }
62
+
63
+ const notification = hooks.Notification;
64
+ if (notification !== undefined && !Array.isArray(notification)) {
65
+ return false;
66
+ }
67
+
68
+ const preToolUse = hooks.PreToolUse;
69
+ return preToolUse === undefined || Array.isArray(preToolUse);
70
+ };
71
+
72
+ const readClaudeSettings = async (templatePath: string): Promise<ClaudeSettings> => {
73
+ const templateRaw = await readFile(templatePath, "utf8");
74
+ const parsed: unknown = JSON.parse(templateRaw);
75
+
76
+ if (!isClaudeSettings(parsed)) {
77
+ throw new Error("Claude settings template has an invalid shape.");
78
+ }
79
+
80
+ return parsed;
81
+ };
82
+
83
+ const mergeStringArrays = (left: string[] = [], right: string[] = []): string[] =>
84
+ Array.from(new Set([...left, ...right]));
85
+
86
+ const upsertHookMatcherEntry = (
87
+ entries: HookMatcherEntry[] | undefined,
88
+ matcher: string,
89
+ command: string,
90
+ ): HookMatcherEntry[] => {
91
+ const nextEntries = [...(entries ?? [])];
92
+ const existingIndex = nextEntries.findIndex((entry) => (entry.matcher ?? "") === matcher);
93
+ const hook: CommandHook = { type: "command", command };
94
+
95
+ if (existingIndex === -1) {
96
+ nextEntries.push({ matcher, hooks: [hook] });
97
+ return nextEntries;
98
+ }
99
+
100
+ const existingEntry = nextEntries[existingIndex];
101
+ nextEntries[existingIndex] = { ...existingEntry, hooks: [hook] };
102
+ return nextEntries;
103
+ };
104
+
105
+ const mergeClaudeSettings = (
106
+ existingSettings: ClaudeSettings,
107
+ baseSettings: ClaudeSettings,
108
+ packagePath: string,
109
+ ): ClaudeSettings => {
110
+ const notificationCommand = `${packagePath}/.claude/hooks/notification.sh`;
111
+ const protectFilesCommand = `${packagePath}/.claude/hooks/protect-files.sh`;
112
+ const statusLineCommand = `node ${packagePath}/.claude/hooks/status-line/index.js`;
113
+
114
+ return {
115
+ ...existingSettings,
116
+ $schema: baseSettings.$schema ?? existingSettings.$schema,
117
+ attribution: { ...(baseSettings.attribution ?? {}), ...(existingSettings.attribution ?? {}) },
118
+ permissions: {
119
+ ...(existingSettings.permissions ?? {}),
120
+ ...(baseSettings.permissions ?? {}),
121
+ ask: mergeStringArrays(baseSettings.permissions?.ask, existingSettings.permissions?.ask),
122
+ deny: mergeStringArrays(baseSettings.permissions?.deny, existingSettings.permissions?.deny),
123
+ },
124
+ hooks: {
125
+ ...existingSettings.hooks,
126
+ Notification: upsertHookMatcherEntry(existingSettings.hooks.Notification, "", notificationCommand),
127
+ PreToolUse: upsertHookMatcherEntry(existingSettings.hooks.PreToolUse, "Edit|Write", protectFilesCommand),
128
+ },
129
+ statusLine: {
130
+ ...existingSettings.statusLine,
131
+ ...baseSettings.statusLine,
132
+ type: "command",
133
+ command: statusLineCommand,
134
+ },
135
+ };
136
+ };
137
+
138
+ export const renderClaude = async (targetDir: string, force: boolean, packageRoot: string): Promise<string[]> => {
139
+ const templatePath = path.join(packageRoot, "claude/.claude/settings.base.json");
140
+ const baseSettings = await readClaudeSettings(templatePath);
141
+ const packagePath = "./node_modules/pasika/claude";
142
+ const notificationEntries = upsertHookMatcherEntry(
143
+ baseSettings.hooks.Notification,
144
+ "",
145
+ `${packagePath}/.claude/hooks/notification.sh`,
146
+ );
147
+ const preToolUseEntries = upsertHookMatcherEntry(
148
+ baseSettings.hooks.PreToolUse,
149
+ "Edit|Write",
150
+ `${packagePath}/.claude/hooks/protect-files.sh`,
151
+ );
152
+ const settings: ClaudeSettings = {
153
+ ...baseSettings,
154
+ hooks: {
155
+ ...baseSettings.hooks,
156
+ Notification: notificationEntries,
157
+ PreToolUse: preToolUseEntries,
158
+ },
159
+ statusLine: {
160
+ ...baseSettings.statusLine,
161
+ type: "command",
162
+ command: `node ${packagePath}/.claude/hooks/status-line/index.js`,
163
+ },
164
+ };
165
+
166
+ const outputDir = path.join(targetDir, ".claude");
167
+ const outputPath = path.join(outputDir, "settings.json");
168
+
169
+ await mkdir(outputDir, { recursive: true });
170
+
171
+ const finalSettings =
172
+ !force && (await fileExists(outputPath))
173
+ ? mergeClaudeSettings(await readClaudeSettings(outputPath), settings, packagePath)
174
+ : settings;
175
+
176
+ await writeFile(outputPath, `${JSON.stringify(finalSettings, null, 2)}\n`, "utf8");
177
+
178
+ return [outputPath];
179
+ };
180
+
181
+ const isDirectRun = process.argv[1] ? path.resolve(process.argv[1]) === __filename : false;
182
+
183
+ if (isDirectRun) {
184
+ const args = process.argv.slice(2);
185
+ const getArgValue = (flag: string): string | undefined => {
186
+ const index = args.indexOf(flag);
187
+ if (index === -1) {
188
+ return undefined;
189
+ }
190
+
191
+ return args[index + 1];
192
+ };
193
+
194
+ const targetDir = path.resolve(process.cwd(), getArgValue("--target-dir") ?? ".");
195
+ const force = args.includes("--force");
196
+ const packageRoot = path.resolve(__dirname, "../..");
197
+
198
+ try {
199
+ const outputs = await renderClaude(targetDir, force, packageRoot);
200
+ for (const output of outputs) {
201
+ process.stdout.write(`Wrote ${output}\n`);
202
+ }
203
+ } catch (error) {
204
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
205
+ process.exit(1);
206
+ }
207
+ }
@@ -0,0 +1,132 @@
1
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { constants } from "node:fs";
4
+ import { fileURLToPath } from "node:url";
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+ const isRecord = (value) => typeof value === "object" && value !== null;
8
+ const fileExists = async (filePath) => {
9
+ try {
10
+ await access(filePath, constants.F_OK);
11
+ return true;
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ };
17
+ const isClaudeSettings = (value) => {
18
+ if (!isRecord(value)) {
19
+ return false;
20
+ }
21
+ const hooks = value.hooks;
22
+ const statusLine = value.statusLine;
23
+ if (!isRecord(hooks) || !isRecord(statusLine) || typeof statusLine.command !== "string") {
24
+ return false;
25
+ }
26
+ const notification = hooks.Notification;
27
+ if (notification !== undefined && !Array.isArray(notification)) {
28
+ return false;
29
+ }
30
+ const preToolUse = hooks.PreToolUse;
31
+ return preToolUse === undefined || Array.isArray(preToolUse);
32
+ };
33
+ const readClaudeSettings = async (templatePath) => {
34
+ const templateRaw = await readFile(templatePath, "utf8");
35
+ const parsed = JSON.parse(templateRaw);
36
+ if (!isClaudeSettings(parsed)) {
37
+ throw new Error("Claude settings template has an invalid shape.");
38
+ }
39
+ return parsed;
40
+ };
41
+ const mergeStringArrays = (left = [], right = []) => Array.from(new Set([...left, ...right]));
42
+ const upsertHookMatcherEntry = (entries, matcher, command) => {
43
+ const nextEntries = [...(entries ?? [])];
44
+ const existingIndex = nextEntries.findIndex((entry) => (entry.matcher ?? "") === matcher);
45
+ const hook = { type: "command", command };
46
+ if (existingIndex === -1) {
47
+ nextEntries.push({ matcher, hooks: [hook] });
48
+ return nextEntries;
49
+ }
50
+ const existingEntry = nextEntries[existingIndex];
51
+ nextEntries[existingIndex] = { ...existingEntry, hooks: [hook] };
52
+ return nextEntries;
53
+ };
54
+ const mergeClaudeSettings = (existingSettings, baseSettings, packagePath) => {
55
+ const notificationCommand = `${packagePath}/.claude/hooks/notification.sh`;
56
+ const protectFilesCommand = `${packagePath}/.claude/hooks/protect-files.sh`;
57
+ const statusLineCommand = `node ${packagePath}/.claude/hooks/status-line/index.js`;
58
+ return {
59
+ ...existingSettings,
60
+ $schema: baseSettings.$schema ?? existingSettings.$schema,
61
+ attribution: { ...(baseSettings.attribution ?? {}), ...(existingSettings.attribution ?? {}) },
62
+ permissions: {
63
+ ...(existingSettings.permissions ?? {}),
64
+ ...(baseSettings.permissions ?? {}),
65
+ ask: mergeStringArrays(baseSettings.permissions?.ask, existingSettings.permissions?.ask),
66
+ deny: mergeStringArrays(baseSettings.permissions?.deny, existingSettings.permissions?.deny),
67
+ },
68
+ hooks: {
69
+ ...existingSettings.hooks,
70
+ Notification: upsertHookMatcherEntry(existingSettings.hooks.Notification, "", notificationCommand),
71
+ PreToolUse: upsertHookMatcherEntry(existingSettings.hooks.PreToolUse, "Edit|Write", protectFilesCommand),
72
+ },
73
+ statusLine: {
74
+ ...existingSettings.statusLine,
75
+ ...baseSettings.statusLine,
76
+ type: "command",
77
+ command: statusLineCommand,
78
+ },
79
+ };
80
+ };
81
+ export const renderClaude = async (targetDir, force, packageRoot) => {
82
+ const templatePath = path.join(packageRoot, "claude/.claude/settings.base.json");
83
+ const baseSettings = await readClaudeSettings(templatePath);
84
+ const packagePath = "./node_modules/pasika/claude";
85
+ const notificationEntries = upsertHookMatcherEntry(baseSettings.hooks.Notification, "", `${packagePath}/.claude/hooks/notification.sh`);
86
+ const preToolUseEntries = upsertHookMatcherEntry(baseSettings.hooks.PreToolUse, "Edit|Write", `${packagePath}/.claude/hooks/protect-files.sh`);
87
+ const settings = {
88
+ ...baseSettings,
89
+ hooks: {
90
+ ...baseSettings.hooks,
91
+ Notification: notificationEntries,
92
+ PreToolUse: preToolUseEntries,
93
+ },
94
+ statusLine: {
95
+ ...baseSettings.statusLine,
96
+ type: "command",
97
+ command: `node ${packagePath}/.claude/hooks/status-line/index.js`,
98
+ },
99
+ };
100
+ const outputDir = path.join(targetDir, ".claude");
101
+ const outputPath = path.join(outputDir, "settings.json");
102
+ await mkdir(outputDir, { recursive: true });
103
+ const finalSettings = !force && (await fileExists(outputPath))
104
+ ? mergeClaudeSettings(await readClaudeSettings(outputPath), settings, packagePath)
105
+ : settings;
106
+ await writeFile(outputPath, `${JSON.stringify(finalSettings, null, 2)}\n`, "utf8");
107
+ return [outputPath];
108
+ };
109
+ const isDirectRun = process.argv[1] ? path.resolve(process.argv[1]) === __filename : false;
110
+ if (isDirectRun) {
111
+ const args = process.argv.slice(2);
112
+ const getArgValue = (flag) => {
113
+ const index = args.indexOf(flag);
114
+ if (index === -1) {
115
+ return undefined;
116
+ }
117
+ return args[index + 1];
118
+ };
119
+ const targetDir = path.resolve(process.cwd(), getArgValue("--target-dir") ?? ".");
120
+ const force = args.includes("--force");
121
+ const packageRoot = path.resolve(__dirname, "../..");
122
+ try {
123
+ const outputs = await renderClaude(targetDir, force, packageRoot);
124
+ for (const output of outputs) {
125
+ process.stdout.write(`Wrote ${output}\n`);
126
+ }
127
+ }
128
+ catch (error) {
129
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
130
+ process.exit(1);
131
+ }
132
+ }
@@ -0,0 +1,7 @@
1
+ import { RuleSeverity, styleguide } from "zirka";
2
+ const { eslintConfig } = styleguide({
3
+ node: RuleSeverity.Error,
4
+ typescript: RuleSeverity.Error,
5
+ ignores: ["dist/**", "node_modules/**", "prettier.config.mjs"],
6
+ });
7
+ export default eslintConfig;
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { renderClaude } from "../claude/scripts/render-settings.js";
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+ const packageRoot = path.basename(path.dirname(__dirname)) === "dist" ? path.resolve(__dirname, "../..") : path.resolve(__dirname, "..");
8
+ const args = process.argv.slice(2);
9
+ const helpText = `pasika
10
+
11
+ Usage:
12
+ pasika claude [--target-dir <path>] [--force]
13
+
14
+ Platforms:
15
+ claude
16
+
17
+ Examples:
18
+ npx pasika claude
19
+ npx pasika claude --force
20
+
21
+ Behavior:
22
+ merges into existing .claude/settings.json by default
23
+ replaces it only when --force is passed
24
+ `;
25
+ const getFlagValue = (flag) => {
26
+ const index = args.indexOf(flag);
27
+ if (index === -1) {
28
+ return undefined;
29
+ }
30
+ return args[index + 1];
31
+ };
32
+ const hasFlag = (flag) => args.includes(flag);
33
+ const platform = args[0];
34
+ if (!platform || hasFlag("--help") || hasFlag("-h")) {
35
+ process.stdout.write(helpText);
36
+ process.exit(platform ? 0 : 1);
37
+ }
38
+ const targetDir = path.resolve(process.cwd(), getFlagValue("--target-dir") ?? ".");
39
+ const force = hasFlag("--force");
40
+ const run = async () => {
41
+ switch (platform) {
42
+ case "claude":
43
+ return renderClaude(targetDir, force, packageRoot);
44
+ default:
45
+ throw new Error(`Unknown platform: ${platform}`);
46
+ }
47
+ };
48
+ try {
49
+ const outputs = await run();
50
+ process.stdout.write(`Initialized ${platform} in ${targetDir}\n`);
51
+ for (const output of outputs) {
52
+ process.stdout.write(`Wrote ${output}\n`);
53
+ }
54
+ }
55
+ catch (error) {
56
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
57
+ process.stderr.write(helpText);
58
+ process.exit(1);
59
+ }
@@ -0,0 +1,30 @@
1
+ # Claude Hooks
2
+
3
+ `pasika` ships the reusable shared Claude hooks.
4
+
5
+ ## `status-line/index.js`
6
+
7
+ Displays a two-line status view with:
8
+
9
+ - model name
10
+ - current folder
11
+ - git branch when available
12
+ - used context percentage as a 10-segment bar
13
+ - session cost
14
+ - session duration
15
+
16
+ ## `notification.sh`
17
+
18
+ Sends a desktop notification when Claude needs attention.
19
+
20
+ - macOS: `terminal-notifier` with click-to-focus VS Code behavior, falls back to `osascript`
21
+ - Linux: `notify-send`
22
+ - Windows: PowerShell message box
23
+
24
+ ## `protect-files.sh`
25
+
26
+ Blocks edits to:
27
+
28
+ - `.env` files
29
+ - `package-lock.json`
30
+ - files inside `.git/`
@@ -0,0 +1,17 @@
1
+ # Merge Behavior
2
+
3
+ `pasika` merges into an existing `.claude/settings.json` by default.
4
+
5
+ It updates the Claude base pieces owned by `pasika`:
6
+
7
+ - `hooks.Notification`
8
+ - `hooks.PreToolUse`
9
+ - `statusLine`
10
+
11
+ It preserves unrelated project-specific settings such as:
12
+
13
+ - custom permissions
14
+ - extra hooks like `PostToolUse`
15
+ - plugin and marketplace config
16
+
17
+ Use `--force` only when you want to replace the file instead of merging.
@@ -0,0 +1,20 @@
1
+ # pasika
2
+
3
+ `pasika` is a reusable Claude Code base package.
4
+
5
+ ## Scope
6
+
7
+ - Claude only for v1
8
+ - shared hooks and base settings
9
+ - merge-friendly project integration
10
+ - no project-specific skills, rules, or plugin choices
11
+
12
+ ## Usage
13
+
14
+ Install `pasika` as a dev dependency, then run:
15
+
16
+ ```bash
17
+ npx pasika claude
18
+ ```
19
+
20
+ That generates or updates `.claude/settings.json` to point shared hooks at `./node_modules/pasika/claude/...`.
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "pasika",
3
+ "version": "0.1.0",
4
+ "description": "Reusable Claude Code base config package",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "scripts": {
8
+ "build": "rm -rf dist AGENTS.md CLAUDE.md && tsc -p tsconfig.json && npx vulyk docs",
9
+ "check": "npm run lint && npm run typecheck",
10
+ "fix": "eslint . --fix",
11
+ "lint": "eslint .",
12
+ "typecheck": "tsc -p tsconfig.json --noEmit",
13
+ "prepack": "npm run build"
14
+ },
15
+ "bin": {
16
+ "pasika": "dist/scripts/pasika.js"
17
+ },
18
+ "files": [
19
+ "claude",
20
+ "docs",
21
+ "dist",
22
+ "AGENTS.md",
23
+ "CLAUDE.md",
24
+ "README.md"
25
+ ],
26
+ "devDependencies": {
27
+ "@types/node": "^24.12.2",
28
+ "eslint": "^9.39.4",
29
+ "prettier": "^3.8.1",
30
+ "typescript": "^5.9.2",
31
+ "vulyk": "^0.6.1",
32
+ "zirka": "^0.0.28"
33
+ }
34
+ }