himotoku 0.1.1 → 0.1.3
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/himotoku.mjs +60 -40
- package/package.json +20 -7
- package/packages/client/dist/assets/index-BJ-wo9zf.js +83 -0
- package/packages/client/dist/assets/index-DHKIuFuW.css +11 -0
- package/packages/client/dist/index.html +2 -2
- package/packages/client/dist/assets/index-Be0EWei7.css +0 -1
- package/packages/client/dist/assets/index-CK2CVDCQ.js +0 -49
package/bin/himotoku.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import path2 from "node:path";
|
|
6
|
-
import { existsSync as existsSync2,
|
|
6
|
+
import { existsSync as existsSync2, readFileSync, statSync } from "node:fs";
|
|
7
7
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
8
8
|
|
|
9
9
|
// packages/server/dist/index.js
|
|
@@ -35,9 +35,7 @@ Respond in the user's language (detect from their message).`;
|
|
|
35
35
|
options: {
|
|
36
36
|
systemPrompt,
|
|
37
37
|
cwd,
|
|
38
|
-
allowedTools: ["Read", "Grep", "Glob"
|
|
39
|
-
permissionMode: "bypassPermissions",
|
|
40
|
-
allowDangerouslySkipPermissions: true,
|
|
38
|
+
allowedTools: ["Read", "Grep", "Glob"],
|
|
41
39
|
includePartialMessages: true,
|
|
42
40
|
maxTurns: 25,
|
|
43
41
|
abortController,
|
|
@@ -45,8 +43,9 @@ Respond in the user's language (detect from their message).`;
|
|
|
45
43
|
}
|
|
46
44
|
});
|
|
47
45
|
let fullContent = "";
|
|
46
|
+
const toolNameMap = /* @__PURE__ */ new Map();
|
|
48
47
|
for await (const message of q) {
|
|
49
|
-
for (const out of mapMessage(message,
|
|
48
|
+
for (const out of mapMessage(message, toolNameMap)) {
|
|
50
49
|
if (out.type === "chat_stream") {
|
|
51
50
|
fullContent += out.content;
|
|
52
51
|
}
|
|
@@ -54,7 +53,7 @@ Respond in the user's language (detect from their message).`;
|
|
|
54
53
|
}
|
|
55
54
|
}
|
|
56
55
|
}
|
|
57
|
-
function* mapMessage(message,
|
|
56
|
+
function* mapMessage(message, toolNameMap) {
|
|
58
57
|
switch (message.type) {
|
|
59
58
|
case "stream_event": {
|
|
60
59
|
const event = message.event;
|
|
@@ -67,6 +66,7 @@ function* mapMessage(message, _fullContent) {
|
|
|
67
66
|
for (const block of message.message.content) {
|
|
68
67
|
if (typeof block === "object" && "type" in block) {
|
|
69
68
|
if (block.type === "tool_use") {
|
|
69
|
+
toolNameMap.set(block.id, block.name);
|
|
70
70
|
yield {
|
|
71
71
|
type: "tool_start",
|
|
72
72
|
tool: block.name,
|
|
@@ -84,12 +84,14 @@ function* mapMessage(message, _fullContent) {
|
|
|
84
84
|
const blocks = Array.isArray(content) ? content : [];
|
|
85
85
|
for (const block of blocks) {
|
|
86
86
|
if (typeof block === "object" && "type" in block && block.type === "tool_result") {
|
|
87
|
+
const toolUseId = "tool_use_id" in block ? block.tool_use_id : "";
|
|
87
88
|
const output = typeof block.content === "string" ? block.content : JSON.stringify(block.content);
|
|
88
89
|
yield {
|
|
89
90
|
type: "tool_result",
|
|
90
|
-
|
|
91
|
+
// #3: Look up actual tool name
|
|
92
|
+
tool: toolNameMap.get(toolUseId) ?? "unknown",
|
|
91
93
|
output: output.slice(0, 2e3),
|
|
92
|
-
toolUseId
|
|
94
|
+
toolUseId
|
|
93
95
|
};
|
|
94
96
|
}
|
|
95
97
|
}
|
|
@@ -102,7 +104,9 @@ function* mapMessage(message, _fullContent) {
|
|
|
102
104
|
yield {
|
|
103
105
|
type: "chat_done",
|
|
104
106
|
fullContent: success.result,
|
|
105
|
-
costUsd: success.total_cost_usd
|
|
107
|
+
costUsd: success.total_cost_usd,
|
|
108
|
+
// #4: Return session_id for conversation continuity
|
|
109
|
+
sessionId: success.session_id
|
|
106
110
|
};
|
|
107
111
|
} else {
|
|
108
112
|
const error = message;
|
|
@@ -122,13 +126,15 @@ function createServer({ port, cwd, clientDistPath: clientDistOverride }) {
|
|
|
122
126
|
const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app });
|
|
123
127
|
app.get("/ws", upgradeWebSocket(() => {
|
|
124
128
|
let abortController = null;
|
|
129
|
+
let isProcessing = false;
|
|
130
|
+
let sessionId;
|
|
125
131
|
return {
|
|
126
132
|
onMessage: async (event, ws) => {
|
|
127
133
|
let data;
|
|
128
134
|
try {
|
|
129
135
|
data = JSON.parse(typeof event.data === "string" ? event.data : event.data.toString());
|
|
130
136
|
} catch {
|
|
131
|
-
ws
|
|
137
|
+
safeSend(ws, { type: "error", message: "Invalid JSON" });
|
|
132
138
|
return;
|
|
133
139
|
}
|
|
134
140
|
if (data.type === "cancel") {
|
|
@@ -136,41 +142,55 @@ function createServer({ port, cwd, clientDistPath: clientDistOverride }) {
|
|
|
136
142
|
return;
|
|
137
143
|
}
|
|
138
144
|
if (data.type === "chat") {
|
|
145
|
+
if (isProcessing) {
|
|
146
|
+
safeSend(ws, {
|
|
147
|
+
type: "error",
|
|
148
|
+
message: "A request is already in progress. Please wait or cancel it first."
|
|
149
|
+
});
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
isProcessing = true;
|
|
139
153
|
abortController = new AbortController();
|
|
140
154
|
try {
|
|
141
155
|
for await (const msg of runAgent({
|
|
142
156
|
prompt: data.content,
|
|
143
157
|
cwd,
|
|
158
|
+
sessionId,
|
|
144
159
|
abortController
|
|
145
160
|
})) {
|
|
146
|
-
|
|
161
|
+
if (msg.type === "chat_done" && msg.sessionId) {
|
|
162
|
+
sessionId = msg.sessionId;
|
|
163
|
+
}
|
|
164
|
+
safeSend(ws, msg);
|
|
147
165
|
}
|
|
148
166
|
} catch (err) {
|
|
149
167
|
if (err.name !== "AbortError") {
|
|
150
|
-
ws
|
|
168
|
+
safeSend(ws, {
|
|
151
169
|
type: "error",
|
|
152
170
|
message: String(err)
|
|
153
|
-
})
|
|
171
|
+
});
|
|
154
172
|
}
|
|
155
173
|
} finally {
|
|
156
174
|
abortController = null;
|
|
175
|
+
isProcessing = false;
|
|
157
176
|
}
|
|
158
177
|
}
|
|
159
178
|
}
|
|
160
179
|
};
|
|
161
180
|
}));
|
|
162
|
-
const
|
|
163
|
-
const clientDistPath = clientDistOverride ?? path.resolve(
|
|
181
|
+
const __dirname2 = path.dirname(fileURLToPath(import.meta.url));
|
|
182
|
+
const clientDistPath = clientDistOverride ?? path.resolve(__dirname2, "../../client/dist");
|
|
164
183
|
if (existsSync(clientDistPath)) {
|
|
165
184
|
app.use("/*", serveStatic({
|
|
166
|
-
root:
|
|
185
|
+
root: clientDistPath,
|
|
186
|
+
rewriteRequestPath: (p) => p
|
|
167
187
|
}));
|
|
168
188
|
app.get("*", serveStatic({
|
|
169
|
-
root:
|
|
189
|
+
root: clientDistPath,
|
|
170
190
|
rewriteRequestPath: () => "/index.html"
|
|
171
191
|
}));
|
|
172
192
|
}
|
|
173
|
-
const server = serve({ fetch: app.fetch, port }, () => {
|
|
193
|
+
const server = serve({ fetch: app.fetch, port, hostname: "127.0.0.1" }, () => {
|
|
174
194
|
console.log(` \u{1F310} http://localhost:${port}`);
|
|
175
195
|
console.log(` Ready! Ask me anything about this codebase.
|
|
176
196
|
`);
|
|
@@ -178,41 +198,41 @@ function createServer({ port, cwd, clientDistPath: clientDistOverride }) {
|
|
|
178
198
|
injectWebSocket(server);
|
|
179
199
|
return server;
|
|
180
200
|
}
|
|
201
|
+
function safeSend(ws, data) {
|
|
202
|
+
if (ws.readyState === void 0 || ws.readyState === 1) {
|
|
203
|
+
try {
|
|
204
|
+
ws.send(JSON.stringify(data));
|
|
205
|
+
} catch {
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
181
209
|
|
|
182
210
|
// src/cli.ts
|
|
211
|
+
var __dirname = path2.dirname(fileURLToPath2(import.meta.url));
|
|
212
|
+
var pkg = JSON.parse(
|
|
213
|
+
readFileSync(path2.resolve(__dirname, "../package.json"), "utf-8")
|
|
214
|
+
);
|
|
183
215
|
var program = new Command();
|
|
184
|
-
program.name("himotoku").version(
|
|
216
|
+
program.name("himotoku").version(pkg.version).description("Unravel any codebase \u2014 AI-powered code exploration via chat").argument("<repo-path>", "Path to the repository to analyze").option("-p, --port <number>", "Port to listen on", "4890").action((repoPath, opts) => {
|
|
185
217
|
const cwd = path2.resolve(repoPath);
|
|
186
218
|
if (!existsSync2(cwd)) {
|
|
187
219
|
console.error(`Error: ${cwd} does not exist`);
|
|
188
220
|
process.exit(1);
|
|
189
221
|
}
|
|
190
|
-
|
|
222
|
+
if (!statSync(cwd).isDirectory()) {
|
|
223
|
+
console.error(`Error: ${cwd} is not a directory`);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
191
226
|
const port = parseInt(opts.port, 10);
|
|
227
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
228
|
+
console.error(`Error: Invalid port number "${opts.port}". Must be 1-65535.`);
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
192
231
|
console.log();
|
|
193
|
-
console.log(
|
|
232
|
+
console.log(`\u{1F4DC} himotoku v${pkg.version}`);
|
|
194
233
|
console.log(` Analyzing: ${cwd}`);
|
|
195
|
-
console.log(` ${fileCount} files found`);
|
|
196
234
|
console.log();
|
|
197
|
-
const __dirname = path2.dirname(fileURLToPath2(import.meta.url));
|
|
198
235
|
const clientDistPath = path2.resolve(__dirname, "../packages/client/dist");
|
|
199
236
|
createServer({ port, cwd, clientDistPath });
|
|
200
237
|
});
|
|
201
|
-
function countFiles(dir, depth = 0) {
|
|
202
|
-
if (depth > 3) return 0;
|
|
203
|
-
try {
|
|
204
|
-
const entries = readdirSync(dir, { withFileTypes: true });
|
|
205
|
-
let count = 0;
|
|
206
|
-
for (const entry of entries) {
|
|
207
|
-
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
208
|
-
if (entry.isFile()) count++;
|
|
209
|
-
else if (entry.isDirectory()) {
|
|
210
|
-
count += countFiles(path2.join(dir, entry.name), depth + 1);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
return count;
|
|
214
|
-
} catch {
|
|
215
|
-
return 0;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
238
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "himotoku",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Unveil any codebase — AI-powered code exploration via chat",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
7
|
+
".",
|
|
7
8
|
"packages/*"
|
|
8
9
|
],
|
|
9
10
|
"scripts": {
|
|
10
|
-
"build": "
|
|
11
|
-
"
|
|
12
|
-
"
|
|
11
|
+
"build": "node scripts/bundle.mjs",
|
|
12
|
+
"changeset": "changeset",
|
|
13
|
+
"version-packages": "changeset version",
|
|
14
|
+
"release": "changeset publish",
|
|
15
|
+
"prepublishOnly": "npx turbo build"
|
|
13
16
|
},
|
|
14
17
|
"bin": {
|
|
15
18
|
"himotoku": "bin/himotoku.mjs"
|
|
@@ -28,6 +31,14 @@
|
|
|
28
31
|
"claude",
|
|
29
32
|
"agent"
|
|
30
33
|
],
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/gipcompany/himotoku.git"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/gipcompany/himotoku",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/gipcompany/himotoku/issues"
|
|
41
|
+
},
|
|
31
42
|
"packageManager": "npm@11.6.2",
|
|
32
43
|
"license": "MIT",
|
|
33
44
|
"dependencies": {
|
|
@@ -35,12 +46,14 @@
|
|
|
35
46
|
"@hono/node-server": "^1.14.0",
|
|
36
47
|
"@hono/node-ws": "^1.1.0",
|
|
37
48
|
"commander": "^13.0.0",
|
|
38
|
-
"hono": "^4.7.0"
|
|
39
|
-
},
|
|
40
|
-
"peerDependencies": {
|
|
49
|
+
"hono": "^4.7.0",
|
|
41
50
|
"zod": "^4.0.0"
|
|
42
51
|
},
|
|
43
52
|
"devDependencies": {
|
|
53
|
+
"@himotoku/core": "*",
|
|
54
|
+
"@himotoku/server": "*",
|
|
55
|
+
"@changesets/changelog-github": "^0.6.0",
|
|
56
|
+
"@changesets/cli": "^2.30.0",
|
|
44
57
|
"@types/node": "^22.0.0",
|
|
45
58
|
"esbuild": "^0.27.4",
|
|
46
59
|
"turbo": "^2.5.0",
|