hoomanjs 1.0.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.
Files changed (96) hide show
  1. package/.github/screenshot.png +0 -0
  2. package/.github/workflows/build-publish.yml +49 -0
  3. package/LICENSE +21 -0
  4. package/README.md +399 -0
  5. package/docker-compose.yml +13 -0
  6. package/package.json +78 -0
  7. package/src/acp/acp-agent.ts +803 -0
  8. package/src/acp/approvals.ts +147 -0
  9. package/src/acp/index.ts +1 -0
  10. package/src/acp/meta/system-prompt.ts +44 -0
  11. package/src/acp/meta/user-id.ts +44 -0
  12. package/src/acp/prompt-invoke.ts +149 -0
  13. package/src/acp/sessions/config-options.ts +56 -0
  14. package/src/acp/sessions/replay.ts +131 -0
  15. package/src/acp/sessions/store.ts +158 -0
  16. package/src/acp/sessions/title.ts +22 -0
  17. package/src/acp/utils/paths.ts +5 -0
  18. package/src/acp/utils/tool-kind.ts +38 -0
  19. package/src/acp/utils/tool-locations.ts +46 -0
  20. package/src/acp/utils/tool-result-content.ts +27 -0
  21. package/src/chat/app.tsx +428 -0
  22. package/src/chat/approvals.ts +96 -0
  23. package/src/chat/components/ApprovalPrompt.tsx +25 -0
  24. package/src/chat/components/ChatMessage.tsx +47 -0
  25. package/src/chat/components/Composer.tsx +39 -0
  26. package/src/chat/components/EmptyChatBanner.tsx +26 -0
  27. package/src/chat/components/ReasoningStrip.tsx +30 -0
  28. package/src/chat/components/Spinner.tsx +34 -0
  29. package/src/chat/components/StatusBar.tsx +65 -0
  30. package/src/chat/components/ThinkingStatus.tsx +128 -0
  31. package/src/chat/components/ToolEvent.tsx +34 -0
  32. package/src/chat/components/Transcript.tsx +34 -0
  33. package/src/chat/components/ascii-logo.ts +11 -0
  34. package/src/chat/components/shared.ts +70 -0
  35. package/src/chat/index.tsx +42 -0
  36. package/src/chat/types.ts +21 -0
  37. package/src/cli.ts +146 -0
  38. package/src/configure/app.tsx +911 -0
  39. package/src/configure/components/BusyScreen.tsx +22 -0
  40. package/src/configure/components/HomeScreen.tsx +43 -0
  41. package/src/configure/components/MenuScreen.tsx +44 -0
  42. package/src/configure/components/PromptForm.tsx +40 -0
  43. package/src/configure/components/SelectMenuItem.tsx +30 -0
  44. package/src/configure/index.tsx +43 -0
  45. package/src/configure/open-in-editor.ts +133 -0
  46. package/src/configure/types.ts +45 -0
  47. package/src/configure/utils.ts +113 -0
  48. package/src/core/agent/index.ts +76 -0
  49. package/src/core/config.ts +157 -0
  50. package/src/core/index.ts +54 -0
  51. package/src/core/mcp/config.ts +80 -0
  52. package/src/core/mcp/index.ts +13 -0
  53. package/src/core/mcp/manager.ts +109 -0
  54. package/src/core/mcp/prefixed-mcp-tool.ts +45 -0
  55. package/src/core/mcp/tools.ts +92 -0
  56. package/src/core/mcp/types.ts +37 -0
  57. package/src/core/memory/index.ts +17 -0
  58. package/src/core/memory/ltm/embed.ts +67 -0
  59. package/src/core/memory/ltm/index.ts +18 -0
  60. package/src/core/memory/ltm/store.ts +376 -0
  61. package/src/core/memory/ltm/tools.ts +146 -0
  62. package/src/core/memory/ltm/types.ts +111 -0
  63. package/src/core/memory/ltm/utils.ts +218 -0
  64. package/src/core/memory/stm/index.ts +17 -0
  65. package/src/core/models/anthropic.ts +53 -0
  66. package/src/core/models/bedrock.ts +54 -0
  67. package/src/core/models/google.ts +51 -0
  68. package/src/core/models/index.ts +16 -0
  69. package/src/core/models/ollama/index.ts +13 -0
  70. package/src/core/models/ollama/strands-ollama.ts +439 -0
  71. package/src/core/models/openai.ts +12 -0
  72. package/src/core/prompts/index.ts +23 -0
  73. package/src/core/prompts/skills.ts +66 -0
  74. package/src/core/prompts/static/fetch.md +33 -0
  75. package/src/core/prompts/static/filesystem.md +38 -0
  76. package/src/core/prompts/static/identity.md +22 -0
  77. package/src/core/prompts/static/ltm.md +39 -0
  78. package/src/core/prompts/static/memory.md +39 -0
  79. package/src/core/prompts/static/shell.md +34 -0
  80. package/src/core/prompts/static/skills.md +19 -0
  81. package/src/core/prompts/static/thinking.md +27 -0
  82. package/src/core/prompts/system.ts +109 -0
  83. package/src/core/skills/index.ts +2 -0
  84. package/src/core/skills/registry.ts +239 -0
  85. package/src/core/skills/tools.ts +80 -0
  86. package/src/core/toolkit.ts +13 -0
  87. package/src/core/tools/fetch.ts +288 -0
  88. package/src/core/tools/filesystem.ts +747 -0
  89. package/src/core/tools/index.ts +5 -0
  90. package/src/core/tools/shell.ts +426 -0
  91. package/src/core/tools/thinking.ts +184 -0
  92. package/src/core/tools/time.ts +121 -0
  93. package/src/core/utils/cwd-context.ts +11 -0
  94. package/src/core/utils/paths.ts +28 -0
  95. package/src/exec/approvals.ts +85 -0
  96. package/tsconfig.json +30 -0
@@ -0,0 +1,288 @@
1
+ import dns from "node:dns/promises";
2
+ import net from "node:net";
3
+ import { Readability } from "@mozilla/readability";
4
+ import { JSDOM } from "jsdom";
5
+ import { tool } from "@strands-agents/sdk";
6
+ import type { JSONValue, ToolContext } from "@strands-agents/sdk";
7
+ import TurndownService from "turndown";
8
+ import { z } from "zod";
9
+
10
+ async function createUserAgent(): Promise<string> {
11
+ const path = new URL("../../../package.json", import.meta.url);
12
+ const pkg = (await Bun.file(path).json()) as {
13
+ name?: string;
14
+ version?: string;
15
+ };
16
+ return `${pkg.name ?? "hoomanjs"}/${pkg.version ?? "0.0.0"}`;
17
+ }
18
+
19
+ const DEFAULT_TIMEOUT_SECONDS = 30;
20
+ const DEFAULT_MAX_LENGTH = 5000;
21
+ const MAX_FETCH_LENGTH = 1_000_000;
22
+
23
+ const turndown = new TurndownService({
24
+ headingStyle: "atx",
25
+ bulletListMarker: "-",
26
+ codeBlockStyle: "fenced",
27
+ });
28
+
29
+ function toJsonValue(value: unknown): JSONValue {
30
+ return JSON.parse(JSON.stringify(value)) as JSONValue;
31
+ }
32
+
33
+ function isHttpUrl(value: string): URL {
34
+ const url = new URL(value);
35
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
36
+ throw new Error("Only http:// and https:// URLs are supported.");
37
+ }
38
+ return url;
39
+ }
40
+
41
+ function isPrivateHostname(hostname: string): boolean {
42
+ const normalized = hostname.toLowerCase();
43
+ return (
44
+ normalized === "localhost" ||
45
+ normalized === "127.0.0.1" ||
46
+ normalized === "::1" ||
47
+ normalized.endsWith(".localhost")
48
+ );
49
+ }
50
+
51
+ function isPrivateIp(address: string): boolean {
52
+ const version = net.isIP(address);
53
+ if (version === 4) {
54
+ const parts = address.split(".").map((part) => Number(part));
55
+ const a = parts[0] ?? -1;
56
+ const b = parts[1] ?? -1;
57
+
58
+ return (
59
+ a === 10 ||
60
+ a === 127 ||
61
+ (a === 172 && b >= 16 && b <= 31) ||
62
+ (a === 192 && b === 168) ||
63
+ (a === 169 && b === 254)
64
+ );
65
+ }
66
+
67
+ if (version === 6) {
68
+ const normalized = address.toLowerCase();
69
+ return (
70
+ normalized === "::1" ||
71
+ normalized.startsWith("fc") ||
72
+ normalized.startsWith("fd") ||
73
+ normalized.startsWith("fe80:")
74
+ );
75
+ }
76
+
77
+ return false;
78
+ }
79
+
80
+ async function assertRemoteUrl(url: URL): Promise<void> {
81
+ if (isPrivateHostname(url.hostname)) {
82
+ throw new Error("Fetching localhost or loopback URLs is not allowed.");
83
+ }
84
+
85
+ const resolved = await dns.lookup(url.hostname, { all: true });
86
+ if (resolved.some((entry) => isPrivateIp(entry.address))) {
87
+ throw new Error("Fetching private-network URLs is not allowed.");
88
+ }
89
+ }
90
+
91
+ function responseHeaders(headers: Headers): Record<string, string> {
92
+ const out: Record<string, string> = {};
93
+ headers.forEach((value, key) => {
94
+ out[key] = value;
95
+ });
96
+ return out;
97
+ }
98
+
99
+ function isHtml(contentType: string, body: string): boolean {
100
+ const normalized = contentType.toLowerCase();
101
+ return (
102
+ normalized.includes("text/html") ||
103
+ normalized.includes("application/xhtml+xml") ||
104
+ /^\s*<!doctype html/i.test(body) ||
105
+ /^\s*<html[\s>]/i.test(body)
106
+ );
107
+ }
108
+
109
+ function tryFormatJson(body: string): string {
110
+ try {
111
+ return JSON.stringify(JSON.parse(body), null, 2);
112
+ } catch {
113
+ return body;
114
+ }
115
+ }
116
+
117
+ function htmlToMarkdown(
118
+ html: string,
119
+ baseUrl: string,
120
+ ): { markdown: string; title?: string | null } {
121
+ const dom = new JSDOM(html, { url: baseUrl });
122
+ const reader = new Readability(dom.window.document);
123
+ const article = reader.parse();
124
+
125
+ const source = article?.content ?? dom.window.document.body?.innerHTML ?? "";
126
+ const markdown = turndown.turndown(source).trim();
127
+
128
+ return {
129
+ markdown: markdown || "Page could not be simplified to markdown.",
130
+ title: article?.title ?? dom.window.document.title ?? null,
131
+ };
132
+ }
133
+
134
+ function sliceContent(
135
+ content: string,
136
+ startIndex: number,
137
+ maxLength: number,
138
+ ): { content: string; truncated: boolean; nextStartIndex: number | null } {
139
+ if (startIndex >= content.length) {
140
+ return {
141
+ content: "No more content available.",
142
+ truncated: false,
143
+ nextStartIndex: null,
144
+ };
145
+ }
146
+
147
+ const slice = content.slice(startIndex, startIndex + maxLength);
148
+ const nextStartIndex =
149
+ startIndex + slice.length < content.length
150
+ ? startIndex + slice.length
151
+ : null;
152
+
153
+ return {
154
+ content: slice,
155
+ truncated: nextStartIndex !== null,
156
+ nextStartIndex,
157
+ };
158
+ }
159
+
160
+ function createFetchInputSchema() {
161
+ return z.object({
162
+ url: z.string().url().describe("Remote HTTP(S) URL to fetch."),
163
+ max_length: z.coerce
164
+ .number()
165
+ .int()
166
+ .min(1)
167
+ .max(MAX_FETCH_LENGTH)
168
+ .optional()
169
+ .describe("Maximum number of characters to return."),
170
+ start_index: z.coerce
171
+ .number()
172
+ .int()
173
+ .min(0)
174
+ .optional()
175
+ .describe("Start returning content from this character index."),
176
+ raw: z
177
+ .boolean()
178
+ .optional()
179
+ .describe(
180
+ "Return raw response text instead of simplifying HTML to markdown.",
181
+ ),
182
+ timeout: z.coerce
183
+ .number()
184
+ .positive()
185
+ .optional()
186
+ .describe("Request timeout in seconds."),
187
+ headers: z
188
+ .record(z.string(), z.string())
189
+ .optional()
190
+ .describe("Optional extra HTTP headers."),
191
+ });
192
+ }
193
+
194
+ export function createFetchTools() {
195
+ const inputSchema = createFetchInputSchema();
196
+
197
+ return [
198
+ tool({
199
+ name: "fetch",
200
+ description:
201
+ "Fetch a remote URL and return response content. HTML pages are simplified to markdown by default to save context window and tokens.",
202
+ inputSchema,
203
+ callback: async (input, context?: ToolContext) => {
204
+ const timeoutSeconds = input.timeout ?? DEFAULT_TIMEOUT_SECONDS;
205
+ const timeoutSignal = AbortSignal.timeout(timeoutSeconds * 1000);
206
+ const signal = context
207
+ ? AbortSignal.any([timeoutSignal, context.agent.cancelSignal])
208
+ : timeoutSignal;
209
+
210
+ const url = isHttpUrl(input.url);
211
+ await assertRemoteUrl(url);
212
+
213
+ const headers = new Headers(input.headers);
214
+ if (!headers.has("user-agent")) {
215
+ headers.set("user-agent", await createUserAgent());
216
+ }
217
+ if (!headers.has("accept")) {
218
+ headers.set(
219
+ "accept",
220
+ "text/html,application/xhtml+xml,application/json,text/plain;q=0.9,*/*;q=0.5",
221
+ );
222
+ }
223
+
224
+ try {
225
+ const response = await fetch(url, {
226
+ method: "GET",
227
+ headers,
228
+ redirect: "follow",
229
+ signal,
230
+ });
231
+
232
+ const text = await response.text();
233
+ const contentType = response.headers.get("content-type") ?? "";
234
+
235
+ if (!response.ok) {
236
+ throw new Error(
237
+ `HTTP ${response.status} ${response.statusText}: GET ${url.toString()}`,
238
+ );
239
+ }
240
+
241
+ let transformed = text;
242
+ let transformedFormat: "raw" | "markdown" | "json-pretty" = "raw";
243
+ let title: string | null | undefined;
244
+
245
+ if (!input.raw && isHtml(contentType, text)) {
246
+ const result = htmlToMarkdown(text, url.toString());
247
+ transformed = result.markdown;
248
+ transformedFormat = "markdown";
249
+ title = result.title;
250
+ } else if (contentType.toLowerCase().includes("json")) {
251
+ transformed = tryFormatJson(text);
252
+ transformedFormat = "json-pretty";
253
+ }
254
+
255
+ const paged = sliceContent(
256
+ transformed,
257
+ input.start_index ?? 0,
258
+ input.max_length ?? DEFAULT_MAX_LENGTH,
259
+ );
260
+
261
+ return toJsonValue({
262
+ url: url.toString(),
263
+ status: response.status,
264
+ statusText: response.statusText,
265
+ headers: responseHeaders(response.headers),
266
+ content_type: contentType || null,
267
+ format: transformedFormat,
268
+ title: title ?? null,
269
+ start_index: input.start_index ?? 0,
270
+ max_length: input.max_length ?? DEFAULT_MAX_LENGTH,
271
+ total_length: transformed.length,
272
+ truncated: paged.truncated,
273
+ next_start_index: paged.nextStartIndex,
274
+ content: paged.content,
275
+ });
276
+ } catch (error) {
277
+ if (error instanceof Error && error.name === "AbortError") {
278
+ const reason = timeoutSignal.aborted
279
+ ? `Request timed out after ${timeoutSeconds} seconds`
280
+ : "Request was cancelled";
281
+ throw new Error(`${reason}: ${url.toString()}`);
282
+ }
283
+ throw error;
284
+ }
285
+ },
286
+ }),
287
+ ];
288
+ }