@coze-arch/cli 0.0.18 → 0.0.19-alpha.502ddf

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 (97) hide show
  1. package/lib/__templates__/expo/.coze +1 -0
  2. package/lib/__templates__/expo/.cozeproj/scripts/validate.sh +8 -0
  3. package/lib/__templates__/expo/package.json +2 -1
  4. package/lib/__templates__/nextjs/.coze +1 -0
  5. package/lib/__templates__/nextjs/package.json +3 -1
  6. package/lib/__templates__/nextjs/scripts/validate.sh +10 -0
  7. package/lib/__templates__/nuxt-vue/.coze +1 -0
  8. package/lib/__templates__/nuxt-vue/app/pages/index.vue +6 -0
  9. package/lib/__templates__/nuxt-vue/eslint.config.mjs +25 -0
  10. package/lib/__templates__/nuxt-vue/nuxt.config.ts +2 -2
  11. package/lib/__templates__/nuxt-vue/package.json +9 -2
  12. package/lib/__templates__/nuxt-vue/pnpm-lock.yaml +790 -10
  13. package/lib/__templates__/nuxt-vue/scripts/validate.sh +10 -0
  14. package/lib/__templates__/pi-agent/.coze +10 -0
  15. package/lib/__templates__/pi-agent/AGENTS.md +149 -0
  16. package/lib/__templates__/pi-agent/README.md +218 -0
  17. package/lib/__templates__/pi-agent/_gitignore +3 -0
  18. package/lib/__templates__/pi-agent/_npmrc +23 -0
  19. package/lib/__templates__/pi-agent/bin/pi-bot.ts +8 -0
  20. package/lib/__templates__/pi-agent/docs/project-overview.md +368 -0
  21. package/lib/__templates__/pi-agent/docs/user/getting-started.md +46 -0
  22. package/lib/__templates__/pi-agent/package.json +63 -0
  23. package/lib/__templates__/pi-agent/pi-resources/SYSTEM.md +15 -0
  24. package/lib/__templates__/pi-agent/pi-resources/extensions/preference-memory/index.ts +355 -0
  25. package/lib/__templates__/pi-agent/pi-resources/skills/coze-asr/SKILL.md +30 -0
  26. package/lib/__templates__/pi-agent/pi-resources/skills/coze-image-gen/SKILL.md +29 -0
  27. package/lib/__templates__/pi-agent/pi-resources/skills/coze-tts/SKILL.md +57 -0
  28. package/lib/__templates__/pi-agent/pi-resources/skills/coze-video-gen/SKILL.md +40 -0
  29. package/lib/__templates__/pi-agent/pnpm-lock.yaml +8282 -0
  30. package/lib/__templates__/pi-agent/scripts/dev.sh +14 -0
  31. package/lib/__templates__/pi-agent/scripts/prepare.sh +35 -0
  32. package/lib/__templates__/pi-agent/src/agent.ts +363 -0
  33. package/lib/__templates__/pi-agent/src/channels/feishu/index.ts +760 -0
  34. package/lib/__templates__/pi-agent/src/channels/feishu/streaming-card.ts +297 -0
  35. package/lib/__templates__/pi-agent/src/channels/wechat/index.ts +171 -0
  36. package/lib/__templates__/pi-agent/src/cli.ts +117 -0
  37. package/lib/__templates__/pi-agent/src/config.ts +749 -0
  38. package/lib/__templates__/pi-agent/src/core.ts +219 -0
  39. package/lib/__templates__/pi-agent/src/dashboard/api/channels.ts +104 -0
  40. package/lib/__templates__/pi-agent/src/dashboard/api/models.ts +98 -0
  41. package/lib/__templates__/pi-agent/src/dashboard/api/overview.ts +33 -0
  42. package/lib/__templates__/pi-agent/src/dashboard/config-store.ts +64 -0
  43. package/lib/__templates__/pi-agent/src/dashboard/index.ts +74 -0
  44. package/lib/__templates__/pi-agent/src/dashboard/server.ts +610 -0
  45. package/lib/__templates__/pi-agent/src/dashboard/types.ts +25 -0
  46. package/lib/__templates__/pi-agent/src/dashboard/web/index.html +13 -0
  47. package/lib/__templates__/pi-agent/src/dashboard/web/postcss.config.cjs +7 -0
  48. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/app-layout.tsx +172 -0
  49. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/page-title.tsx +17 -0
  50. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/alert.tsx +22 -0
  51. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/badge.tsx +25 -0
  52. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/button.tsx +40 -0
  53. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/card.tsx +29 -0
  54. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/input.tsx +18 -0
  55. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/label.tsx +8 -0
  56. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/select.tsx +80 -0
  57. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/separator.tsx +23 -0
  58. package/lib/__templates__/pi-agent/src/dashboard/web/src/hooks/use-fetch.ts +32 -0
  59. package/lib/__templates__/pi-agent/src/dashboard/web/src/hooks/use-local-storage-state.ts +23 -0
  60. package/lib/__templates__/pi-agent/src/dashboard/web/src/main.tsx +24 -0
  61. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/chat-page.tsx +440 -0
  62. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/overview-page.tsx +330 -0
  63. package/lib/__templates__/pi-agent/src/dashboard/web/src/services/chat-ws-service.ts +167 -0
  64. package/lib/__templates__/pi-agent/src/dashboard/web/src/styles.css +203 -0
  65. package/lib/__templates__/pi-agent/src/dashboard/web/src/utils/index.ts +11 -0
  66. package/lib/__templates__/pi-agent/src/dashboard/web/tsconfig.json +13 -0
  67. package/lib/__templates__/pi-agent/src/dashboard/web/vite.config.ts +17 -0
  68. package/lib/__templates__/pi-agent/src/index.ts +123 -0
  69. package/lib/__templates__/pi-agent/src/pi-resources.ts +125 -0
  70. package/lib/__templates__/pi-agent/src/session-store.ts +223 -0
  71. package/lib/__templates__/pi-agent/src/tools/common/format-coze-error.ts +12 -0
  72. package/lib/__templates__/pi-agent/src/tools/index.ts +2 -0
  73. package/lib/__templates__/pi-agent/src/tools/web-fetch/index.ts +195 -0
  74. package/lib/__templates__/pi-agent/src/tools/web-search/index.ts +206 -0
  75. package/lib/__templates__/pi-agent/template.config.js +45 -0
  76. package/lib/__templates__/pi-agent/tests/cli.test.ts +136 -0
  77. package/lib/__templates__/pi-agent/tests/config.test.ts +377 -0
  78. package/lib/__templates__/pi-agent/tests/dashboard-models-api.test.ts +171 -0
  79. package/lib/__templates__/pi-agent/tests/feishu-channel.test.ts +149 -0
  80. package/lib/__templates__/pi-agent/tests/feishu-streaming-card.test.ts +15 -0
  81. package/lib/__templates__/pi-agent/tests/pi-resources.test.ts +73 -0
  82. package/lib/__templates__/pi-agent/tests/preference-memory.test.ts +43 -0
  83. package/lib/__templates__/pi-agent/tests/session-store.test.ts +61 -0
  84. package/lib/__templates__/pi-agent/tests/smoke/run-smoke.ts +275 -0
  85. package/lib/__templates__/pi-agent/tests/web-fetch.test.ts +157 -0
  86. package/lib/__templates__/pi-agent/tests/web-search.test.ts +208 -0
  87. package/lib/__templates__/pi-agent/tsconfig.json +21 -0
  88. package/lib/__templates__/pi-agent/types/larksuiteoapi-node-sdk.d.ts +113 -0
  89. package/lib/__templates__/taro/.coze +1 -0
  90. package/lib/__templates__/taro/.cozeproj/scripts/validate.sh +8 -0
  91. package/lib/__templates__/taro/package.json +1 -1
  92. package/lib/__templates__/templates.json +24 -0
  93. package/lib/__templates__/vite/.coze +1 -0
  94. package/lib/__templates__/vite/package.json +3 -1
  95. package/lib/__templates__/vite/scripts/validate.sh +10 -0
  96. package/lib/cli.js +13 -2
  97. package/package.json +1 -1
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "jsx": "react-jsx",
5
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "moduleResolution": "Bundler",
8
+ "types": ["vite/client"],
9
+ "noEmit": true
10
+ },
11
+ "include": ["vite.config.ts", "src/**/*.ts", "src/**/*.tsx"]
12
+ }
13
+
@@ -0,0 +1,17 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+ import { fileURLToPath } from "node:url";
4
+ import { dirname, resolve } from "node:path";
5
+
6
+ const DIR = dirname(fileURLToPath(import.meta.url));
7
+
8
+ export default defineConfig({
9
+ root: DIR,
10
+ plugins: [react()],
11
+ base: "/",
12
+ build: {
13
+ outDir: resolve(DIR, "dist"),
14
+ emptyOutDir: true,
15
+ },
16
+ });
17
+
@@ -0,0 +1,123 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import type { BotAppConfig } from "./core.js";
3
+ import type { FeishuChannel } from "./channels/feishu/index.js";
4
+ import { createFeishuChannel } from "./channels/feishu/index.js";
5
+ import type { WechatChannel } from "./channels/wechat/index.js";
6
+ import { createWechatChannel } from "./channels/wechat/index.js";
7
+ import { loadBotAppConfig } from "./config.js";
8
+ import { createAgentRuntime } from "./agent.js";
9
+ import type { DashboardServer } from "./dashboard/types.js";
10
+ import { createDashboard } from "./dashboard/index.js";
11
+ import type { ConfigStore } from "./dashboard/config-store.js";
12
+
13
+ export interface BotApp {
14
+ config: BotAppConfig;
15
+ channels: {
16
+ feishu?: FeishuChannel;
17
+ wechat?: WechatChannel;
18
+ };
19
+ dashboard: DashboardServer;
20
+ start(): Promise<void>;
21
+ stop(): Promise<void>;
22
+ }
23
+
24
+ export async function createBotApp(
25
+ config: BotAppConfig = loadBotAppConfig(),
26
+ options: { dashboardConfigStore?: ConfigStore } = {},
27
+ ): Promise<BotApp> {
28
+ const runtime = await createAgentRuntime(config.agent);
29
+ const channels: BotApp["channels"] = {};
30
+
31
+ if (config.channels.feishu?.enabled) {
32
+ channels.feishu = createFeishuChannel(
33
+ {
34
+ appId: config.channels.feishu.appId,
35
+ appSecret: config.channels.feishu.appSecret,
36
+ domain: config.channels.feishu.domain,
37
+ encryptKey: config.channels.feishu.encryptKey,
38
+ verificationToken: config.channels.feishu.verificationToken,
39
+ routing: {
40
+ groupRequireMention: config.routing.feishuGroupRequireMention
41
+ }
42
+ },
43
+ {
44
+ async onMessage(message) {
45
+ const text = await runtime.run(message);
46
+ return { text };
47
+ },
48
+ async onStreamMessage(message, handlers) {
49
+ const result = await runtime.stream(message, {
50
+ onMeta: handlers.onMeta,
51
+ onDelta: handlers.onDelta,
52
+ onError: handlers.onError
53
+ });
54
+ return { text: result.finalText };
55
+ }
56
+ }
57
+ );
58
+ }
59
+
60
+ if (config.channels.wechat?.enabled) {
61
+ channels.wechat = createWechatChannel(
62
+ {
63
+ routing: {
64
+ groupRequireMention: config.routing.wechatGroupRequireMention
65
+ }
66
+ },
67
+ {
68
+ async onMessage(message) {
69
+ const text = await runtime.run(message);
70
+ return { text };
71
+ }
72
+ }
73
+ );
74
+ }
75
+
76
+ const dashboard = createDashboard({
77
+ botConfig: config,
78
+ channels,
79
+ agentRuntime: runtime,
80
+ configStore: options.dashboardConfigStore,
81
+ });
82
+
83
+ return {
84
+ config,
85
+ channels,
86
+ dashboard,
87
+ async start() {
88
+ await Promise.all([
89
+ ...Object.values(channels).map((channel) => channel.start()),
90
+ dashboard.start()
91
+ ]);
92
+ },
93
+ async stop() {
94
+ await Promise.all([
95
+ ...Object.values(channels).map((channel) => channel.stop()),
96
+ dashboard.stop()
97
+ ]);
98
+ await runtime.dispose();
99
+ }
100
+ };
101
+ }
102
+
103
+ export async function main(): Promise<void> {
104
+ const config = loadBotAppConfig();
105
+ const app = await createBotApp(config);
106
+ await app.start();
107
+
108
+ console.log(`[${config.appName}] started`);
109
+ console.log(`Feishu enabled: ${Boolean(app.channels.feishu)}`);
110
+ console.log(`WeChat enabled: ${Boolean(app.channels.wechat)}`);
111
+ console.log(`Dashboard: ${app.dashboard.getUrl()}`);
112
+ console.log(`Agent mode: ${config.agent.mode}`);
113
+ console.log(`Workspace dir: ${config.agent.cwd}`);
114
+ console.log(`Agent dir: ${config.agent.agentDir}`);
115
+ }
116
+
117
+ const entrypoint = process.argv[1];
118
+ if (entrypoint && fileURLToPath(import.meta.url) === entrypoint) {
119
+ main().catch((error: unknown) => {
120
+ console.error(error);
121
+ process.exit(1);
122
+ });
123
+ }
@@ -0,0 +1,125 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { DefaultResourceLoader, type SettingsManager } from "@mariozechner/pi-coding-agent";
4
+ import { getProjectRoot } from "./config.js";
5
+
6
+ type PiResourceLoaderArgs = {
7
+ cwd: string;
8
+ agentDir?: string;
9
+ settingsManager: SettingsManager;
10
+ };
11
+
12
+ export function getPiResourcesDir(): string {
13
+ return join(getProjectRoot(), "pi-resources");
14
+ }
15
+
16
+ export function buildPiResourceLoaderOptions(
17
+ args: PiResourceLoaderArgs
18
+ ): ConstructorParameters<typeof DefaultResourceLoader>[0] {
19
+ const piResourcesDir = getPiResourcesDir();
20
+ const repoSystemPromptPath = join(piResourcesDir, "SYSTEM.md");
21
+ const repoAppendSystemPromptPath = join(piResourcesDir, "APPEND_SYSTEM.md");
22
+ const localSystemPromptPath = join(args.cwd, ".pi", "SYSTEM.md");
23
+ const localAppendSystemPromptPath = join(args.cwd, ".pi", "APPEND_SYSTEM.md");
24
+ const systemPromptSource = pickFirstExistingPath([localSystemPromptPath, repoSystemPromptPath]);
25
+ const appendSystemPromptSource = pickFirstExistingPath([
26
+ localAppendSystemPromptPath,
27
+ repoAppendSystemPromptPath
28
+ ]);
29
+
30
+ return {
31
+ cwd: args.cwd,
32
+ agentDir: args.agentDir,
33
+ settingsManager: args.settingsManager,
34
+ additionalExtensionPaths: discoverExtensionEntries(join(piResourcesDir, "extensions")),
35
+ additionalSkillPaths: existingPaths([join(piResourcesDir, "skills")]),
36
+ additionalPromptTemplatePaths: existingPaths([join(piResourcesDir, "prompts")]),
37
+ // Keep the bot runtime independent from repo/workspace AGENTS.md files.
38
+ agentsFilesOverride: () => ({ agentsFiles: [] }),
39
+ // Make source precedence explicit instead of expressing it indirectly through
40
+ // override callbacks: workspace/.pi wins, then repo-managed resources, then the
41
+ // loader's own defaults when neither exists.
42
+ systemPrompt: systemPromptSource,
43
+ appendSystemPrompt: appendSystemPromptSource
44
+ };
45
+ }
46
+
47
+ export function createPiResourceLoader(args: PiResourceLoaderArgs): DefaultResourceLoader {
48
+ return new DefaultResourceLoader(buildPiResourceLoaderOptions(args));
49
+ }
50
+
51
+ function existingPaths(paths: string[]): string[] {
52
+ return paths.filter((path) => existsSync(path));
53
+ }
54
+
55
+ function pickFirstExistingPath(paths: string[]): string | undefined {
56
+ return paths.find((path) => existsSync(path));
57
+ }
58
+
59
+ function discoverExtensionEntries(dir: string): string[] {
60
+ if (!existsSync(dir)) {
61
+ return [];
62
+ }
63
+
64
+ const discovered = new Set<string>();
65
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
66
+ if (entry.name.startsWith(".")) {
67
+ continue;
68
+ }
69
+
70
+ const entryPath = join(dir, entry.name);
71
+ if (entry.isFile() && isExtensionModule(entry.name)) {
72
+ discovered.add(entryPath);
73
+ continue;
74
+ }
75
+
76
+ if (!entry.isDirectory()) {
77
+ continue;
78
+ }
79
+
80
+ for (const extensionEntry of resolveExtensionEntries(entryPath)) {
81
+ discovered.add(extensionEntry);
82
+ }
83
+ }
84
+
85
+ return Array.from(discovered);
86
+ }
87
+
88
+ function resolveExtensionEntries(dir: string): string[] {
89
+ const packageJsonPath = join(dir, "package.json");
90
+ if (existsSync(packageJsonPath)) {
91
+ const manifest = readPiManifest(packageJsonPath);
92
+ if (manifest?.extensions?.length) {
93
+ return manifest.extensions
94
+ .map((extensionPath) => join(dir, extensionPath))
95
+ .filter((extensionPath) => existsSync(extensionPath));
96
+ }
97
+ }
98
+
99
+ const indexTs = join(dir, "index.ts");
100
+ if (existsSync(indexTs)) {
101
+ return [indexTs];
102
+ }
103
+
104
+ const indexJs = join(dir, "index.js");
105
+ if (existsSync(indexJs)) {
106
+ return [indexJs];
107
+ }
108
+
109
+ return [];
110
+ }
111
+
112
+ function readPiManifest(packageJsonPath: string): { extensions?: string[] } | undefined {
113
+ try {
114
+ const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as {
115
+ pi?: { extensions?: string[] };
116
+ };
117
+ return pkg.pi;
118
+ } catch {
119
+ return undefined;
120
+ }
121
+ }
122
+
123
+ function isExtensionModule(fileName: string): boolean {
124
+ return fileName.endsWith(".ts") || fileName.endsWith(".js");
125
+ }
@@ -0,0 +1,223 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { CURRENT_SESSION_VERSION } from "@mariozechner/pi-coding-agent";
5
+
6
+ export type SessionRecord = {
7
+ sessionKey: string;
8
+ keyHash: string;
9
+ sessionId: string;
10
+ sessionFile: string;
11
+ updatedAt: number;
12
+ };
13
+
14
+ type IndexFileShapeV1 = {
15
+ version: 1;
16
+ updatedAt: number;
17
+ sessions: Record<
18
+ string,
19
+ {
20
+ keyHash: string;
21
+ sessionId: string;
22
+ sessionFile: string;
23
+ updatedAt: number;
24
+ }
25
+ >;
26
+ };
27
+
28
+ function sha256Hex(input: string): string {
29
+ return createHash("sha256").update(input).digest("hex");
30
+ }
31
+
32
+ function safeRename(from: string, to: string): void {
33
+ try {
34
+ fs.renameSync(from, to);
35
+ } catch {
36
+ // best-effort
37
+ }
38
+ }
39
+
40
+ function ensureDir(dirPath: string): void {
41
+ fs.mkdirSync(dirPath, { recursive: true });
42
+ }
43
+
44
+ function readJsonFile(filePath: string): unknown {
45
+ const raw = fs.readFileSync(filePath, "utf-8");
46
+ return JSON.parse(raw) as unknown;
47
+ }
48
+
49
+ function writeFileAtomic(filePath: string, content: string, mode: number): void {
50
+ const dir = path.dirname(filePath);
51
+ ensureDir(dir);
52
+ const tmpPath = `${filePath}.tmp`;
53
+ fs.writeFileSync(tmpPath, content, { encoding: "utf-8", mode });
54
+ fs.renameSync(tmpPath, filePath);
55
+ }
56
+
57
+ function ensureTranscriptHeader(params: { sessionFile: string; sessionId: string }): void {
58
+ if (fs.existsSync(params.sessionFile)) {
59
+ return;
60
+ }
61
+ ensureDir(path.dirname(params.sessionFile));
62
+ const header = {
63
+ type: "session",
64
+ version: CURRENT_SESSION_VERSION,
65
+ id: params.sessionId,
66
+ timestamp: new Date().toISOString(),
67
+ cwd: process.cwd(),
68
+ };
69
+ fs.writeFileSync(params.sessionFile, `${JSON.stringify(header)}\n`, {
70
+ encoding: "utf-8",
71
+ mode: 0o600,
72
+ });
73
+ }
74
+
75
+ function decodeIndexFileShapeV1(value: unknown): IndexFileShapeV1 | null {
76
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
77
+ const rec = value as Record<string, unknown>;
78
+ if (rec.version !== 1) return null;
79
+ if (!("sessions" in rec) || !rec.sessions || typeof rec.sessions !== "object" || Array.isArray(rec.sessions)) {
80
+ return null;
81
+ }
82
+ const updatedAt = typeof rec.updatedAt === "number" ? rec.updatedAt : Date.now();
83
+ const sessions = rec.sessions as Record<string, unknown>;
84
+ const out: IndexFileShapeV1["sessions"] = {};
85
+ for (const [sessionKey, entry] of Object.entries(sessions)) {
86
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
87
+ const e = entry as Record<string, unknown>;
88
+ const keyHash = typeof e.keyHash === "string" ? e.keyHash : "";
89
+ const sessionId = typeof e.sessionId === "string" ? e.sessionId : "";
90
+ const sessionFile = typeof e.sessionFile === "string" ? e.sessionFile : "";
91
+ const entryUpdatedAt = typeof e.updatedAt === "number" ? e.updatedAt : updatedAt;
92
+ if (!sessionKey || !keyHash || !sessionId || !sessionFile) continue;
93
+ out[sessionKey] = { keyHash, sessionId, sessionFile, updatedAt: entryUpdatedAt };
94
+ }
95
+ return { version: 1, updatedAt, sessions: out };
96
+ }
97
+
98
+ export class SessionStore {
99
+ private readonly sessionsDir: string;
100
+ private readonly indexPath: string;
101
+ private readonly transcriptsDir: string;
102
+ private readonly archiveDir: string;
103
+ private readonly cache = new Map<string, SessionRecord>();
104
+
105
+ constructor(private readonly agentDir: string) {
106
+ this.sessionsDir = path.join(agentDir, ".pi-bot", "sessions");
107
+ this.indexPath = path.join(this.sessionsDir, "index.json");
108
+ this.transcriptsDir = path.join(this.sessionsDir, "transcripts");
109
+ this.archiveDir = path.join(this.sessionsDir, "archive");
110
+ }
111
+
112
+ load(): void {
113
+ this.cache.clear();
114
+
115
+ if (!fs.existsSync(this.indexPath)) {
116
+ return;
117
+ }
118
+
119
+ try {
120
+ const decoded = decodeIndexFileShapeV1(readJsonFile(this.indexPath));
121
+ if (!decoded) {
122
+ throw new Error("invalid index.json schema");
123
+ }
124
+ for (const [sessionKey, entry] of Object.entries(decoded.sessions)) {
125
+ this.cache.set(sessionKey, {
126
+ sessionKey,
127
+ keyHash: entry.keyHash,
128
+ sessionId: entry.sessionId,
129
+ sessionFile: entry.sessionFile,
130
+ updatedAt: entry.updatedAt,
131
+ });
132
+ }
133
+ } catch {
134
+ // Keep a corrupt copy for debugging instead of crashing startup.
135
+ safeRename(this.indexPath, `${this.indexPath}.corrupt-${Date.now()}`);
136
+ this.cache.clear();
137
+ }
138
+ }
139
+
140
+ listSessionKeys(): string[] {
141
+ return Array.from(this.cache.keys());
142
+ }
143
+
144
+ get(sessionKey: string): SessionRecord | undefined {
145
+ return this.cache.get(sessionKey);
146
+ }
147
+
148
+ ensureSession(sessionKey: string): SessionRecord {
149
+ const existing = this.cache.get(sessionKey);
150
+ if (existing) {
151
+ // If the transcript was deleted externally, recreate the header so the
152
+ // session remains recoverable.
153
+ ensureTranscriptHeader({ sessionFile: existing.sessionFile, sessionId: existing.sessionId });
154
+ return existing;
155
+ }
156
+
157
+ const now = Date.now();
158
+ const keyHash = sha256Hex(sessionKey);
159
+ const sessionId = randomUUID();
160
+ const sessionFile = path.join(this.transcriptsDir, `${keyHash}-${sessionId}.jsonl`);
161
+ const record: SessionRecord = {
162
+ sessionKey,
163
+ keyHash,
164
+ sessionId,
165
+ sessionFile,
166
+ updatedAt: now,
167
+ };
168
+ this.cache.set(sessionKey, record);
169
+ ensureTranscriptHeader({ sessionFile, sessionId });
170
+ this.saveIndex();
171
+ return record;
172
+ }
173
+
174
+ resetSession(sessionKey: string): SessionRecord {
175
+ const prior = this.cache.get(sessionKey);
176
+ if (prior && fs.existsSync(prior.sessionFile)) {
177
+ ensureDir(this.archiveDir);
178
+ const baseName = path.basename(prior.sessionFile);
179
+ const archivedBase = baseName.endsWith(".jsonl") ? baseName : `${baseName}.jsonl`;
180
+ const archivedPathCandidate = path.join(this.archiveDir, archivedBase);
181
+ const archivedPath = fs.existsSync(archivedPathCandidate)
182
+ ? path.join(this.archiveDir, `${prior.keyHash}-${prior.sessionId}-${Date.now()}.jsonl`)
183
+ : archivedPathCandidate;
184
+ safeRename(prior.sessionFile, archivedPath);
185
+ }
186
+
187
+ // Keep keyHash stable for this sessionKey, rotate sessionId + sessionFile.
188
+ const now = Date.now();
189
+ const keyHash = prior?.keyHash ?? sha256Hex(sessionKey);
190
+ const sessionId = randomUUID();
191
+ const sessionFile = path.join(this.transcriptsDir, `${keyHash}-${sessionId}.jsonl`);
192
+ const next: SessionRecord = {
193
+ sessionKey,
194
+ keyHash,
195
+ sessionId,
196
+ sessionFile,
197
+ updatedAt: now,
198
+ };
199
+ this.cache.set(sessionKey, next);
200
+ ensureTranscriptHeader({ sessionFile, sessionId });
201
+ this.saveIndex();
202
+ return next;
203
+ }
204
+
205
+ private saveIndex(): void {
206
+ const now = Date.now();
207
+ const sessions: IndexFileShapeV1["sessions"] = {};
208
+ for (const [sessionKey, entry] of this.cache.entries()) {
209
+ sessions[sessionKey] = {
210
+ keyHash: entry.keyHash,
211
+ sessionId: entry.sessionId,
212
+ sessionFile: entry.sessionFile,
213
+ updatedAt: entry.updatedAt,
214
+ };
215
+ }
216
+ const payload: IndexFileShapeV1 = {
217
+ version: 1,
218
+ updatedAt: now,
219
+ sessions,
220
+ };
221
+ writeFileAtomic(this.indexPath, JSON.stringify(payload, null, 2), 0o600);
222
+ }
223
+ }
@@ -0,0 +1,12 @@
1
+ export function formatCozeError(error: unknown): string {
2
+ if (error && typeof error === "object" && "message" in error) {
3
+ const message = String(error.message);
4
+ const status =
5
+ "statusCode" in error && typeof error.statusCode === "number" ? ` (${error.statusCode})` : "";
6
+ return `${message}${status}`;
7
+ }
8
+ if (error instanceof Error) {
9
+ return error.message;
10
+ }
11
+ return String(error);
12
+ }
@@ -0,0 +1,2 @@
1
+ export { cozeWebSearchTool } from "./web-search/index.js";
2
+ export { cozeWebFetchTool } from "./web-fetch/index.js";