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.
- package/.claude/settings.local.json +29 -0
- package/.github/workflows/npm.yml +23 -0
- package/package.json +29 -0
- package/pnpm-workspace.yaml +5 -0
- package/src/Binome.tsx +23 -0
- package/src/Markdown.tsx +24 -0
- package/src/cli.tsx +13 -0
- package/src/marked-terminal.d.ts +14 -0
- package/src/useSession.ts +105 -0
- package/src/useWatcher.ts +42 -0
- package/tsconfig.json +28 -0
|
@@ -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
|
+
}
|
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
|
+
};
|
package/src/Markdown.tsx
ADDED
|
@@ -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
|
+
}
|