himotoku 0.1.0 → 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/README.md CHANGED
@@ -1 +1 @@
1
- # deveil
1
+ # himotoku
@@ -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, readdirSync } from "node:fs";
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
@@ -19,7 +19,7 @@ import { serveStatic } from "@hono/node-server/serve-static";
19
19
  import { query } from "@anthropic-ai/claude-agent-sdk";
20
20
  async function* runAgent(options) {
21
21
  const { prompt, cwd, sessionId, abortController } = options;
22
- const systemPrompt = `You are deveil \u2014 an AI agent that unveils codebases.
22
+ const systemPrompt = `You are himotoku \u2014 an AI agent that unravels codebases.
23
23
  You are analyzing the repository at: ${cwd}
24
24
 
25
25
  When answering questions:
@@ -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", "Bash"],
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, fullContent)) {
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, _fullContent) {
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
- tool: "unknown",
91
+ // #3: Look up actual tool name
92
+ tool: toolNameMap.get(toolUseId) ?? "unknown",
91
93
  output: output.slice(0, 2e3),
92
- toolUseId: "tool_use_id" in block ? block.tool_use_id : ""
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;
@@ -118,17 +122,24 @@ function* mapMessage(message, _fullContent) {
118
122
 
119
123
  // packages/server/dist/index.js
120
124
  function createServer({ port, cwd, clientDistPath: clientDistOverride }) {
125
+ if (!process.env.ANTHROPIC_API_KEY) {
126
+ console.error("Error: ANTHROPIC_API_KEY environment variable is not set.");
127
+ console.error(" export ANTHROPIC_API_KEY=sk-ant-...");
128
+ process.exit(1);
129
+ }
121
130
  const app = new Hono();
122
131
  const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app });
123
132
  app.get("/ws", upgradeWebSocket(() => {
124
133
  let abortController = null;
134
+ let isProcessing = false;
135
+ let sessionId;
125
136
  return {
126
137
  onMessage: async (event, ws) => {
127
138
  let data;
128
139
  try {
129
140
  data = JSON.parse(typeof event.data === "string" ? event.data : event.data.toString());
130
141
  } catch {
131
- ws.send(JSON.stringify({ type: "error", message: "Invalid JSON" }));
142
+ safeSend(ws, { type: "error", message: "Invalid JSON" });
132
143
  return;
133
144
  }
134
145
  if (data.type === "cancel") {
@@ -136,41 +147,55 @@ function createServer({ port, cwd, clientDistPath: clientDistOverride }) {
136
147
  return;
137
148
  }
138
149
  if (data.type === "chat") {
150
+ if (isProcessing) {
151
+ safeSend(ws, {
152
+ type: "error",
153
+ message: "A request is already in progress. Please wait or cancel it first."
154
+ });
155
+ return;
156
+ }
157
+ isProcessing = true;
139
158
  abortController = new AbortController();
140
159
  try {
141
160
  for await (const msg of runAgent({
142
161
  prompt: data.content,
143
162
  cwd,
163
+ sessionId,
144
164
  abortController
145
165
  })) {
146
- ws.send(JSON.stringify(msg));
166
+ if (msg.type === "chat_done" && msg.sessionId) {
167
+ sessionId = msg.sessionId;
168
+ }
169
+ safeSend(ws, msg);
147
170
  }
148
171
  } catch (err) {
149
172
  if (err.name !== "AbortError") {
150
- ws.send(JSON.stringify({
173
+ safeSend(ws, {
151
174
  type: "error",
152
175
  message: String(err)
153
- }));
176
+ });
154
177
  }
155
178
  } finally {
156
179
  abortController = null;
180
+ isProcessing = false;
157
181
  }
158
182
  }
159
183
  }
160
184
  };
161
185
  }));
162
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
163
- const clientDistPath = clientDistOverride ?? path.resolve(__dirname, "../../client/dist");
186
+ const __dirname2 = path.dirname(fileURLToPath(import.meta.url));
187
+ const clientDistPath = clientDistOverride ?? path.resolve(__dirname2, "../../client/dist");
164
188
  if (existsSync(clientDistPath)) {
165
189
  app.use("/*", serveStatic({
166
- root: path.relative(process.cwd(), clientDistPath)
190
+ root: clientDistPath,
191
+ rewriteRequestPath: (p) => p
167
192
  }));
168
193
  app.get("*", serveStatic({
169
- root: path.relative(process.cwd(), clientDistPath),
194
+ root: clientDistPath,
170
195
  rewriteRequestPath: () => "/index.html"
171
196
  }));
172
197
  }
173
- const server = serve({ fetch: app.fetch, port }, () => {
198
+ const server = serve({ fetch: app.fetch, port, hostname: "127.0.0.1" }, () => {
174
199
  console.log(` \u{1F310} http://localhost:${port}`);
175
200
  console.log(` Ready! Ask me anything about this codebase.
176
201
  `);
@@ -178,41 +203,41 @@ function createServer({ port, cwd, clientDistPath: clientDistOverride }) {
178
203
  injectWebSocket(server);
179
204
  return server;
180
205
  }
206
+ function safeSend(ws, data) {
207
+ if (ws.readyState === void 0 || ws.readyState === 1) {
208
+ try {
209
+ ws.send(JSON.stringify(data));
210
+ } catch {
211
+ }
212
+ }
213
+ }
181
214
 
182
215
  // src/cli.ts
216
+ var __dirname = path2.dirname(fileURLToPath2(import.meta.url));
217
+ var pkg = JSON.parse(
218
+ readFileSync(path2.resolve(__dirname, "../package.json"), "utf-8")
219
+ );
183
220
  var program = new Command();
184
- program.name("himotoku").version("0.1.0").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) => {
221
+ 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
222
  const cwd = path2.resolve(repoPath);
186
223
  if (!existsSync2(cwd)) {
187
224
  console.error(`Error: ${cwd} does not exist`);
188
225
  process.exit(1);
189
226
  }
190
- const fileCount = countFiles(cwd);
227
+ if (!statSync(cwd).isDirectory()) {
228
+ console.error(`Error: ${cwd} is not a directory`);
229
+ process.exit(1);
230
+ }
191
231
  const port = parseInt(opts.port, 10);
232
+ if (isNaN(port) || port < 1 || port > 65535) {
233
+ console.error(`Error: Invalid port number "${opts.port}". Must be 1-65535.`);
234
+ process.exit(1);
235
+ }
192
236
  console.log();
193
- console.log("\u{1F4DC} himotoku v0.1.0");
237
+ console.log(`\u{1F4DC} himotoku v${pkg.version}`);
194
238
  console.log(` Analyzing: ${cwd}`);
195
- console.log(` ${fileCount} files found`);
196
239
  console.log();
197
- const __dirname = path2.dirname(fileURLToPath2(import.meta.url));
198
240
  const clientDistPath = path2.resolve(__dirname, "../packages/client/dist");
199
241
  createServer({ port, cwd, clientDistPath });
200
242
  });
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
243
  program.parse();
package/package.json CHANGED
@@ -1,21 +1,24 @@
1
1
  {
2
2
  "name": "himotoku",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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": "turbo build && node scripts/bundle.mjs",
11
- "dev": "turbo dev",
12
- "clean": "turbo clean"
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
- "himotoku": "bin/deveil.mjs"
18
+ "himotoku": "bin/himotoku.mjs"
16
19
  },
17
20
  "files": [
18
- "bin/deveil.mjs",
21
+ "bin/himotoku.mjs",
19
22
  "packages/client/dist"
20
23
  ],
21
24
  "engines": {
@@ -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",