binome 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,29 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(npm run *)",
5
+ "Bash(timeout 25 npm run start ./src)",
6
+ "Bash(echo \"EXIT: $?\")",
7
+ "Read(//tmp/**)",
8
+ "Bash(npx tsc *)",
9
+ "Bash(node -e \"console.log\\('ink', require\\('./node_modules/ink/package.json'\\).version\\); console.log\\('marked', require\\('./node_modules/marked/package.json'\\).version\\); console.log\\('marked-terminal', require\\('./node_modules/marked-terminal/package.json'\\).version\\)\")",
10
+ "Bash(pnpm remove *)",
11
+ "Bash(pnpm add *)",
12
+ "Bash(command -v corepack pnpm npx)",
13
+ "Read(//home/valentin/.local/share/pnpm/**)",
14
+ "Bash(corepack pnpm *)",
15
+ "Bash(corepack pnpm@10 remove ink-markdown)",
16
+ "Bash(corepack pnpm@10 add marked@^9 marked-terminal@^6)",
17
+ "Bash(corepack pnpm@10 add -D @types/marked-terminal@^6)",
18
+ "Bash(pnpm ls *)",
19
+ "Bash(node *)",
20
+ "Bash(npm view *)",
21
+ "Bash(echo \"tsc-exit: $?\")",
22
+ "Bash(command -v corepack npm npx)",
23
+ "Bash(PNPM_HOME=/home/valentin/.local/share/pnpm PATH=/home/valentin/.local/share/pnpm:__TRACKED_VAR__ corepack pnpm remove @types/marked-terminal)",
24
+ "Bash(PNPM_HOME=/home/valentin/.local/share/pnpm PATH=/home/valentin/.local/share/pnpm:__TRACKED_VAR__ corepack pnpm add marked-terminal@^7)",
25
+ "Bash(PNPM_HOME=/home/valentin/.local/share/pnpm PATH=/home/valentin/.local/share/pnpm:__TRACKED_VAR__ corepack pnpm install)",
26
+ "Bash(CI=true PNPM_HOME=/home/valentin/.local/share/pnpm PATH=/home/valentin/.local/share/pnpm:__TRACKED_VAR__ corepack pnpm install --no-frozen-lockfile)"
27
+ ]
28
+ }
29
+ }
@@ -0,0 +1,23 @@
1
+ name: npm
2
+ on:
3
+ create:
4
+ tags:
5
+ - v*
6
+
7
+ jobs:
8
+ build:
9
+ name: Build and publish
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - name: Check out code
13
+ uses: actions/checkout@v3
14
+ - name: Setup Node.js environment
15
+ uses: actions/setup-node@v3
16
+ with:
17
+ node-version: 24
18
+ cache: "npm"
19
+ registry-url: "https://registry.npmjs.org"
20
+ - name: Publish
21
+ run: npm publish
22
+ env:
23
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "binome",
3
+ "version": "0.1.2",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "keywords": [],
7
+ "author": "",
8
+ "license": "ISC",
9
+ "type": "module",
10
+ "dependencies": {
11
+ "@anthropic-ai/claude-agent-sdk": "^0.3.177",
12
+ "@parcel/watcher": "^2.5.6",
13
+ "chalk": "^5.6.2",
14
+ "ink": "^7.0.6",
15
+ "marked": "^15.0.12",
16
+ "marked-terminal": "^7.3.0",
17
+ "react": "^19.2.7"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^25.9.3",
21
+ "@types/react": "^19.2.17",
22
+ "tsx": "^4.22.4",
23
+ "typescript": "^6.0.3"
24
+ },
25
+ "scripts": {
26
+ "dev": "tsx --watch src/cli.tsx",
27
+ "start": "tsx src/cli.tsx"
28
+ }
29
+ }
@@ -0,0 +1,5 @@
1
+ allowBuilds:
2
+ '@parcel/watcher': set this to true or false
3
+ esbuild: set this to true or false
4
+ onlyBuiltDependencies:
5
+ - '@parcel/watcher'
package/src/Binome.tsx ADDED
@@ -0,0 +1,23 @@
1
+ import { Box, Text } from "ink";
2
+ import { Markdown } from "./Markdown.js";
3
+ import { useWatcher } from "./useWatcher.js";
4
+
5
+ type BinomeProps = {
6
+ toWatch: string;
7
+ sessionPurpose: string;
8
+ };
9
+
10
+ export const Binome = ({ toWatch, sessionPurpose }: BinomeProps) => {
11
+ const { messages } = useWatcher(toWatch, sessionPurpose);
12
+
13
+ return (
14
+ <Box flexDirection="column" gap={1} marginBottom={2}>
15
+ <Text>Watching {toWatch}</Text>
16
+ {messages.map((message, index) => (
17
+ <Markdown key={index} dim={index < messages.length - 1}>
18
+ {message}
19
+ </Markdown>
20
+ ))}
21
+ </Box>
22
+ );
23
+ };
@@ -0,0 +1,24 @@
1
+ import { Text } from "ink";
2
+ import { markedTerminal } from "marked-terminal";
3
+ import { Marked } from "marked";
4
+ import chalk from "chalk";
5
+
6
+ const vivid = new Marked().use(markedTerminal());
7
+ const dimmed = new Marked().use(
8
+ markedTerminal({
9
+ text: chalk.gray,
10
+ em: chalk.gray,
11
+ strong: chalk.gray,
12
+ codespan: chalk.gray,
13
+ code: chalk.gray,
14
+ }),
15
+ );
16
+
17
+ type MarkdownProps = {
18
+ children: string;
19
+ dim: boolean;
20
+ };
21
+
22
+ export const Markdown = ({ children, dim }: MarkdownProps) => (
23
+ <Text>{(dim ? dimmed : vivid).parse(children, { async: false }).trim()}</Text>
24
+ );
package/src/cli.tsx ADDED
@@ -0,0 +1,13 @@
1
+ import { render } from "ink";
2
+ import { Binome } from "./Binome.js";
3
+
4
+ const [, , folder, ...purpose] = process.argv;
5
+
6
+ if (!folder) {
7
+ throw "You need to specify a folder to watch";
8
+ }
9
+ if (purpose.length === 0) {
10
+ throw "You need to specify the purpose of your session";
11
+ }
12
+
13
+ render(<Binome toWatch={folder} sessionPurpose={purpose.join(" ")} />);
@@ -0,0 +1,14 @@
1
+ // marked-terminal v7 ships no types, and @types/marked-terminal (max v6) wrongly types
2
+ // markedTerminal() as a TerminalRenderer — at runtime it returns a MarkedExtension for .use().
3
+ // Remove this file if @types ever ships a correct v7.
4
+ declare module "marked-terminal" {
5
+ import type { MarkedExtension } from "marked";
6
+ import type { ChalkInstance } from "chalk";
7
+
8
+ type Styler = ChalkInstance | ((text: string) => string);
9
+
10
+ export const markedTerminal: (
11
+ options?: Partial<Record<string, Styler>>,
12
+ highlightOptions?: object,
13
+ ) => MarkedExtension;
14
+ }
@@ -0,0 +1,105 @@
1
+ import { CanUseTool, query } from "@anthropic-ai/claude-agent-sdk";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+
4
+ export const useSession = (workSubject: string) => {
5
+ const [readFiles, setReadFiles] = useState(new Set<string>());
6
+
7
+ const sessionId = useRef<null | string>(null);
8
+ const ready = useRef(false);
9
+
10
+ const [messages, setMessages] = useState<Array<string>>([]);
11
+
12
+ const canUseTool = useCallback<CanUseTool>(
13
+ async (toolName, input) => {
14
+ switch (toolName) {
15
+ case "Read":
16
+ setReadFiles(readFiles.add(input["file_path"] as string));
17
+ return { behavior: "allow", updatedInput: input };
18
+ case "Bash":
19
+ case "Grep":
20
+ return { behavior: "allow", updatedInput: input };
21
+ default:
22
+ console.error("tool not supported", toolName, input);
23
+ return { behavior: "deny", message: "This is not supported yet" };
24
+ }
25
+ },
26
+ [setReadFiles],
27
+ );
28
+
29
+ useEffect(() => {
30
+ void new Promise(async () => {
31
+ for await (const message of query({
32
+ prompt: [
33
+ `I am going to work on: ${workSubject}`,
34
+ "I will provide you with the changes as I make them",
35
+ "Review what I write, and point out anything you think could be improved",
36
+ "Try to infer my intent from the changes, and do not point out things I will most likely work on next",
37
+ "Do not hesitate to visit other files to better grasp the context of my task",
38
+ "You will keep your comments short, concise and to the point",
39
+ "You will not use elaborate sentences to appear eloquent, you will not compliment me",
40
+ 'If you have nothing to comment, say "nothing to say"',
41
+ 'Only say "nothing to say" when you have no comment at all',
42
+ 'Do not say fillers like "Nothing else stands out"',
43
+ "You will not comment on issues covered by a linter, a compiler or automated tests",
44
+ 'Do not announce what you will do, like "Let me check ..."',
45
+ "Reiterate what we are going to work on",
46
+ ].join(". "),
47
+ options: {
48
+ pathToClaudeCodeExecutable:
49
+ process.env["CLAUDE_CODE_PATH"] ?? "claude",
50
+ canUseTool,
51
+ },
52
+ })) {
53
+ if (message.type === "result") {
54
+ sessionId.current = message.session_id;
55
+ }
56
+
57
+ if (message.type === "assistant" && message.message?.content) {
58
+ for (const block of message.message.content) {
59
+ if ("text" in block) {
60
+ setMessages((messages) => messages.concat(block.text));
61
+ ready.current = true;
62
+ } else if ("name" in block) {
63
+ console.log(`Tool: ${block.name}`);
64
+ }
65
+ }
66
+ } else if (message.type === "result") {
67
+ console.log(`Done: ${message.subtype}`);
68
+ }
69
+ }
70
+ });
71
+ }, []);
72
+
73
+ const onFileEvent = (event: string) => (filepath: string) => {
74
+ const session = sessionId.current;
75
+
76
+ if (ready.current && session)
77
+ void new Promise(async () => {
78
+ for await (const message of query({
79
+ prompt: `This file just got ${event}: ${filepath}`,
80
+ options: {
81
+ pathToClaudeCodeExecutable:
82
+ process.env["CLAUDE_CODE_PATH"] ?? "claude",
83
+ resume: session,
84
+ canUseTool,
85
+ },
86
+ })) {
87
+ if (message.type === "assistant" && message.message?.content) {
88
+ for (const block of message.message.content) {
89
+ if ("text" in block) {
90
+ setMessages((messages) => messages.concat(block.text));
91
+ }
92
+ }
93
+ } else if (message.type === "result") {
94
+ console.log(`Done: ${message.subtype}`);
95
+ }
96
+ }
97
+ });
98
+ };
99
+
100
+ return {
101
+ messages,
102
+ onFileUpdated: onFileEvent("updated"),
103
+ onFileCreated: onFileEvent("created"),
104
+ };
105
+ };
@@ -0,0 +1,42 @@
1
+ import watcher, { AsyncSubscription } from "@parcel/watcher";
2
+ import { useEffect } from "react";
3
+ import { useSession } from "./useSession.js";
4
+
5
+ export const useWatcher = (folderToWatch: string, sessionPurpose: string) => {
6
+ const { messages, onFileUpdated, onFileCreated } = useSession(sessionPurpose);
7
+
8
+ useEffect(() => {
9
+ let subscription: AsyncSubscription | null = null;
10
+ let done = false;
11
+
12
+ watcher
13
+ .subscribe(folderToWatch, (_error, events) => {
14
+ for (const event of events) {
15
+ switch (event.type) {
16
+ case "update":
17
+ onFileUpdated(event.path);
18
+ break;
19
+ case "create":
20
+ onFileCreated(event.path);
21
+ break;
22
+ }
23
+ }
24
+ })
25
+ .then((sub) => {
26
+ if (done) {
27
+ sub.unsubscribe();
28
+ } else {
29
+ subscription = sub;
30
+ }
31
+ });
32
+
33
+ return () => {
34
+ done = true;
35
+ if (subscription) {
36
+ subscription.unsubscribe();
37
+ }
38
+ };
39
+ }, []);
40
+
41
+ return { messages };
42
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "node20",
4
+ "moduleResolution": "node16",
5
+ "moduleDetection": "force",
6
+ "target": "esnext",
7
+ "lib": ["DOM", "DOM.Iterable", "ES2023"],
8
+ "jsx": "react-jsx",
9
+ "declaration": true,
10
+ "newLine": "lf",
11
+ "stripInternal": true,
12
+ "erasableSyntaxOnly": true,
13
+ "strict": true,
14
+ "noImplicitReturns": true,
15
+ "noImplicitOverride": true,
16
+ "noUnusedLocals": true,
17
+ "noUnusedParameters": true,
18
+ "noFallthroughCasesInSwitch": true,
19
+ "noUncheckedIndexedAccess": true,
20
+ "noPropertyAccessFromIndexSignature": true,
21
+ "noUncheckedSideEffectImports": true,
22
+ "noEmitOnError": true,
23
+ "useDefineForClassFields": true,
24
+ "forceConsistentCasingInFileNames": true,
25
+ "skipLibCheck": true
26
+ },
27
+ "include": ["src"]
28
+ }