@nghyane/arcane 0.1.16 → 0.1.17

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 (43) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/package.json +7 -15
  3. package/src/config/settings-schema.ts +19 -46
  4. package/src/config/settings.ts +0 -1
  5. package/src/exa/mcp-client.ts +57 -2
  6. package/src/internal-urls/docs-index.generated.ts +1 -2
  7. package/src/internal-urls/index.ts +2 -4
  8. package/src/internal-urls/router.ts +2 -2
  9. package/src/internal-urls/types.ts +2 -2
  10. package/src/mcp/oauth-flow.ts +1 -1
  11. package/src/modes/controllers/command-controller.ts +4 -44
  12. package/src/patch/hashline.ts +42 -0
  13. package/src/prompts/system/system-prompt.md +14 -10
  14. package/src/prompts/thread-extract.md +16 -0
  15. package/src/prompts/tools/render-mermaid.md +9 -0
  16. package/src/sdk.ts +1 -19
  17. package/src/session/agent-session.ts +4 -3
  18. package/src/session/retry-utils.ts +1 -1
  19. package/src/session/session-index.ts +329 -0
  20. package/src/slash-commands/builtin-registry.ts +0 -16
  21. package/src/task/index.ts +1 -1
  22. package/src/tools/ask.ts +9 -6
  23. package/src/tools/bash-skill-urls.ts +3 -3
  24. package/src/tools/create-tools.ts +26 -0
  25. package/src/tools/find-thread.ts +120 -0
  26. package/src/tools/index.ts +5 -0
  27. package/src/tools/read-thread.ts +409 -0
  28. package/src/tools/read.ts +2 -2
  29. package/src/tools/render-mermaid.ts +68 -0
  30. package/src/tools/save-memory.ts +182 -0
  31. package/src/web/search/index.ts +2 -0
  32. package/src/web/search/provider.ts +3 -0
  33. package/src/web/search/providers/anthropic.ts +1 -0
  34. package/src/web/search/providers/gemini.ts +122 -37
  35. package/src/web/search/providers/kagi.ts +163 -0
  36. package/src/web/search/types.ts +1 -0
  37. package/src/internal-urls/memory-protocol.ts +0 -133
  38. package/src/memories/index.ts +0 -1099
  39. package/src/memories/storage.ts +0 -563
  40. package/src/prompts/memories/consolidation.md +0 -30
  41. package/src/prompts/memories/read_path.md +0 -11
  42. package/src/prompts/memories/stage_one_input.md +0 -6
  43. package/src/prompts/memories/stage_one_system.md +0 -21
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Kagi Web Search Provider
3
+ *
4
+ * Calls Kagi's Search API (v0) and maps results into the unified
5
+ * SearchResponse shape used by the web search tool.
6
+ */
7
+ import { getEnvApiKey } from "@nghyane/arcane-ai";
8
+ import type { SearchResponse, SearchSource } from "../types";
9
+ import { SearchProviderError } from "../types";
10
+ import type { SearchParams } from "./base";
11
+ import { SearchProvider } from "./base";
12
+
13
+ const KAGI_SEARCH_URL = "https://kagi.com/api/v0/search";
14
+ const DEFAULT_NUM_RESULTS = 10;
15
+ const MAX_NUM_RESULTS = 40;
16
+
17
+ interface KagiSearchResult {
18
+ t: 0;
19
+ url: string;
20
+ title: string;
21
+ snippet?: string;
22
+ published?: string;
23
+ thumbnail?: {
24
+ url: string;
25
+ width?: number | null;
26
+ height?: number | null;
27
+ };
28
+ }
29
+
30
+ interface KagiRelatedSearches {
31
+ t: 1;
32
+ list: string[];
33
+ }
34
+
35
+ type KagiSearchObject = KagiSearchResult | KagiRelatedSearches;
36
+
37
+ interface KagiMeta {
38
+ id: string;
39
+ node: string;
40
+ ms: number;
41
+ api_balance?: number;
42
+ }
43
+
44
+ interface KagiSearchResponse {
45
+ meta: KagiMeta;
46
+ data: KagiSearchObject[];
47
+ error?: Array<{ code: number; msg: string; ref?: unknown }>;
48
+ }
49
+
50
+ function clampNumResults(value: number | undefined): number {
51
+ if (!value || Number.isNaN(value)) return DEFAULT_NUM_RESULTS;
52
+ return Math.min(MAX_NUM_RESULTS, Math.max(1, value));
53
+ }
54
+
55
+ function dateToAgeSeconds(dateStr: string | null | undefined): number | undefined {
56
+ if (!dateStr) return undefined;
57
+ try {
58
+ const date = new Date(dateStr);
59
+ if (Number.isNaN(date.getTime())) return undefined;
60
+ return Math.floor((Date.now() - date.getTime()) / 1000);
61
+ } catch {
62
+ return undefined;
63
+ }
64
+ }
65
+
66
+ /** Find KAGI_API_KEY from environment or .env files. */
67
+ export function findApiKey(): string | null {
68
+ return getEnvApiKey("kagi") ?? null;
69
+ }
70
+
71
+ async function callKagiSearch(
72
+ apiKey: string,
73
+ query: string,
74
+ limit: number,
75
+ signal?: AbortSignal,
76
+ ): Promise<KagiSearchResponse> {
77
+ const url = new URL(KAGI_SEARCH_URL);
78
+ url.searchParams.set("q", query);
79
+ url.searchParams.set("limit", String(limit));
80
+
81
+ const response = await fetch(url, {
82
+ headers: {
83
+ Authorization: `Bot ${apiKey}`,
84
+ Accept: "application/json",
85
+ },
86
+ signal,
87
+ });
88
+
89
+ if (!response.ok) {
90
+ const errorText = await response.text();
91
+ throw new SearchProviderError("kagi", `Kagi API error (${response.status}): ${errorText}`, response.status);
92
+ }
93
+
94
+ const data = (await response.json()) as KagiSearchResponse;
95
+
96
+ if (data.error && data.error.length > 0) {
97
+ const firstError = data.error[0];
98
+ throw new SearchProviderError("kagi", `Kagi API error: ${firstError.msg}`, firstError.code);
99
+ }
100
+
101
+ return data;
102
+ }
103
+
104
+ /** Execute Kagi web search. */
105
+ export async function searchKagi(params: {
106
+ query: string;
107
+ num_results?: number;
108
+ signal?: AbortSignal;
109
+ }): Promise<SearchResponse> {
110
+ const numResults = clampNumResults(params.num_results);
111
+ const apiKey = findApiKey();
112
+ if (!apiKey) {
113
+ throw new Error("KAGI_API_KEY not found. Set it in environment or .env file.");
114
+ }
115
+
116
+ const data = await callKagiSearch(apiKey, params.query, numResults, params.signal);
117
+
118
+ const sources: SearchSource[] = [];
119
+ const relatedQuestions: string[] = [];
120
+
121
+ for (const item of data.data) {
122
+ if (item.t === 0) {
123
+ sources.push({
124
+ title: item.title,
125
+ url: item.url,
126
+ snippet: item.snippet,
127
+ publishedDate: item.published ?? undefined,
128
+ ageSeconds: dateToAgeSeconds(item.published),
129
+ });
130
+ } else if (item.t === 1) {
131
+ relatedQuestions.push(...item.list);
132
+ }
133
+ }
134
+
135
+ return {
136
+ provider: "kagi",
137
+ sources: sources.slice(0, numResults),
138
+ relatedQuestions: relatedQuestions.length > 0 ? relatedQuestions : undefined,
139
+ requestId: data.meta.id,
140
+ };
141
+ }
142
+
143
+ /** Search provider for Kagi web search. */
144
+ export class KagiProvider extends SearchProvider {
145
+ readonly id = "kagi";
146
+ readonly label = "Kagi";
147
+
148
+ isAvailable() {
149
+ try {
150
+ return !!findApiKey();
151
+ } catch {
152
+ return false;
153
+ }
154
+ }
155
+
156
+ search(params: SearchParams): Promise<SearchResponse> {
157
+ return searchKagi({
158
+ query: params.query,
159
+ num_results: params.numSearchResults ?? params.limit,
160
+ signal: params.signal,
161
+ });
162
+ }
163
+ }
@@ -15,6 +15,7 @@ export type SearchProviderId =
15
15
  | "perplexity"
16
16
  | "gemini"
17
17
  | "codex"
18
+ | "kagi"
18
19
  | "synthetic";
19
20
 
20
21
  /** Source returned by search (all providers) */
@@ -1,133 +0,0 @@
1
- import * as fs from "node:fs/promises";
2
- import * as path from "node:path";
3
- import { isEnoent } from "@nghyane/arcane-utils";
4
- import { validateRelativePath } from "./skill-protocol";
5
- import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
6
-
7
- const DEFAULT_MEMORY_FILE = "memory_summary.md";
8
- const MEMORY_NAMESPACE = "root";
9
-
10
- /**
11
- * Options for the memory:// URL protocol.
12
- */
13
- export interface MemoryProtocolOptions {
14
- /**
15
- * Returns the absolute path to the current project's memory root.
16
- */
17
- getMemoryRoot: () => string;
18
- }
19
-
20
- function ensureWithinRoot(targetPath: string, rootPath: string): void {
21
- if (targetPath !== rootPath && !targetPath.startsWith(`${rootPath}${path.sep}`)) {
22
- throw new Error("memory:// URL escapes memory root");
23
- }
24
- }
25
-
26
- function toMemoryValidationError(error: unknown): Error {
27
- const message = error instanceof Error ? error.message : String(error);
28
- return new Error(message.replace("skill://", "memory://"));
29
- }
30
-
31
- /**
32
- * Resolve a memory:// URL to an absolute filesystem path under memory root.
33
- */
34
- export function resolveMemoryUrlToPath(url: InternalUrl, memoryRoot: string): string {
35
- const namespace = url.rawHost || url.hostname;
36
- if (!namespace) {
37
- throw new Error("memory:// URL requires a namespace: memory://root");
38
- }
39
- if (namespace !== MEMORY_NAMESPACE) {
40
- throw new Error(`Unknown memory namespace: ${namespace}. Supported: ${MEMORY_NAMESPACE}`);
41
- }
42
-
43
- const rawPathname = url.rawPathname ?? url.pathname;
44
- const hasPath = rawPathname && rawPathname !== "/" && rawPathname !== "";
45
- if (!hasPath) {
46
- return path.resolve(memoryRoot, DEFAULT_MEMORY_FILE);
47
- }
48
- let relativePath: string;
49
- try {
50
- relativePath = decodeURIComponent(rawPathname.slice(1));
51
- } catch {
52
- throw new Error(`Invalid URL encoding in memory:// path: ${url.href}`);
53
- }
54
-
55
- try {
56
- validateRelativePath(relativePath);
57
- } catch (error) {
58
- throw toMemoryValidationError(error);
59
- }
60
-
61
- return path.resolve(memoryRoot, relativePath);
62
- }
63
-
64
- /**
65
- * Protocol handler for memory:// URLs.
66
- *
67
- * URL forms:
68
- * - memory://root - Reads memory_summary.md
69
- * - memory://root/<path> - Reads a relative file under memory root
70
- */
71
- export class MemoryProtocolHandler implements ProtocolHandler {
72
- readonly scheme = "memory";
73
-
74
- constructor(private readonly options: MemoryProtocolOptions) {}
75
-
76
- async resolve(url: InternalUrl): Promise<InternalResource> {
77
- const memoryRoot = path.resolve(this.options.getMemoryRoot());
78
- let resolvedRoot: string;
79
- try {
80
- resolvedRoot = await fs.realpath(memoryRoot);
81
- } catch (error) {
82
- if (isEnoent(error)) {
83
- throw new Error(
84
- "Memory artifacts are not available for this project yet. Run a session with memories enabled first.",
85
- );
86
- }
87
- throw error;
88
- }
89
-
90
- const targetPath = resolveMemoryUrlToPath(url, resolvedRoot);
91
- ensureWithinRoot(targetPath, resolvedRoot);
92
-
93
- const parentDir = path.dirname(targetPath);
94
- try {
95
- const realParent = await fs.realpath(parentDir);
96
- ensureWithinRoot(realParent, resolvedRoot);
97
- } catch (error) {
98
- if (!isEnoent(error)) {
99
- throw error;
100
- }
101
- }
102
-
103
- let realTargetPath: string;
104
- try {
105
- realTargetPath = await fs.realpath(targetPath);
106
- } catch (error) {
107
- if (isEnoent(error)) {
108
- throw new Error(`Memory file not found: ${url.href}`);
109
- }
110
- throw error;
111
- }
112
-
113
- ensureWithinRoot(realTargetPath, resolvedRoot);
114
-
115
- const stat = await fs.stat(realTargetPath);
116
- if (!stat.isFile()) {
117
- throw new Error(`memory:// URL must resolve to a file: ${url.href}`);
118
- }
119
-
120
- const content = await Bun.file(realTargetPath).text();
121
- const ext = path.extname(realTargetPath).toLowerCase();
122
- const contentType: InternalResource["contentType"] = ext === ".md" ? "text/markdown" : "text/plain";
123
-
124
- return {
125
- url: url.href,
126
- content,
127
- contentType,
128
- size: Buffer.byteLength(content, "utf-8"),
129
- sourcePath: realTargetPath,
130
- notes: [],
131
- };
132
- }
133
- }