gencow 0.1.146 → 0.1.147

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/gencow.mjs CHANGED
@@ -39,6 +39,7 @@ import {
39
39
  updateEnvLocalUrl,
40
40
  } from "../lib/cli-project-runtime.mjs";
41
41
  import { createCodegenCommand } from "../lib/codegen-command.mjs";
42
+ import { maybeCheckCliVersion } from "../lib/cli-version-check.mjs";
42
43
  import { updateComponentReadme } from "../lib/component-readme.mjs";
43
44
  import { createConfigCommand } from "../lib/config-command.mjs";
44
45
  import { createCorsCommand } from "../lib/cors-command.mjs";
@@ -81,6 +82,8 @@ import {
81
82
  import { platformFetch, requireCreds, rpcMutation, rpcQuery } from "../lib/platform-client.mjs";
82
83
 
83
84
  const __dirname = dirname(fileURLToPath(import.meta.url));
85
+ const CLI_PACKAGE_JSON = JSON.parse(readFileSync(resolve(__dirname, "..", "package.json"), "utf8"));
86
+ const CLI_VERSION = CLI_PACKAGE_JSON.version || "0.0.0";
84
87
  const _drizzleKitCmd = (subcmd, options = {}) => buildDrizzleKitCommand(subcmd, options);
85
88
  const generateApiTs = createApiCodegenRuntime({});
86
89
  const runAddCommand = createAddCommand({
@@ -282,6 +285,7 @@ const commands = {
282
285
  help() {
283
286
  log(`
284
287
  ${BOLD}${CYAN}Gencow CLI${RESET} ${DIM}— AI Backend Engine${RESET}
288
+ ${DIM}Version: ${CLI_VERSION}${RESET}
285
289
 
286
290
  ${BOLD}Usage:${RESET}
287
291
  gencow <command>
@@ -382,6 +386,10 @@ ${BOLD}Examples:${RESET}
382
386
  `);
383
387
  },
384
388
 
389
+ version() {
390
+ log(CLI_VERSION);
391
+ },
392
+
385
393
  // ── login/logout/whoami ───────────────────────────────
386
394
  login: runLoginCommand,
387
395
  logout: runLogoutCommand,
@@ -455,7 +463,15 @@ ${BOLD}Examples:${RESET}
455
463
 
456
464
  // ─── updateReadme: gencow/ 폴더 스캔 → README에 컴포넌트 문서 자동 추가 ────
457
465
 
458
- const [, , cmd = "help", ...args] = process.argv;
466
+ const [, , rawCmd = "help", ...args] = process.argv;
467
+ const cmd = rawCmd === "--help" || rawCmd === "-h" ? "help" : rawCmd === "--version" || rawCmd === "-v" ? "version" : rawCmd;
468
+ maybeCheckCliVersion({
469
+ command: cmd,
470
+ currentVersion: CLI_VERSION,
471
+ errorImpl: error,
472
+ processEnv: process.env,
473
+ warnImpl: warn,
474
+ });
459
475
 
460
476
  if (commands[cmd]) {
461
477
  Promise.resolve(commands[cmd](...args)).catch((e) => {
@@ -0,0 +1,69 @@
1
+ export const REQUIRED_CLI_TARBALL_FILES = Object.freeze([
2
+ "bin/gencow.mjs",
3
+ "bin/gencow-mcp.mjs",
4
+ "lib/readme-codegen.mjs",
5
+ "lib/deploy-auditor.mjs",
6
+ "lib/dev-cloud-bundle.mjs",
7
+ "lib/codegen/index.mjs",
8
+ "server/index.js",
9
+ "core/index.js",
10
+ "dashboard/index.html",
11
+ ]);
12
+
13
+ const FORBIDDEN_CODEGEN_CORE_IMPORTS = Object.freeze([
14
+ {
15
+ id: "server-only-procedure-route-collector",
16
+ pattern: /(?:import\s*{[^}]*\bcollectProcedureRouteMaps\b[^}]*}\s*from\s*["']@gencow\/core["']|collectProcedureRouteMaps[\s\S]{0,240}from\s*["']@gencow\/core["'])/,
17
+ message: "@gencow/core에서 server-only procedure route collector를 import합니다",
18
+ },
19
+ ]);
20
+
21
+ export function validateCliTarballFiles(files, requiredFiles = REQUIRED_CLI_TARBALL_FILES) {
22
+ const fileSet = new Set(files);
23
+ const errors = [];
24
+ const forbiddenSourceFiles = files.filter((path) => path === "src" || path.startsWith("src/"));
25
+
26
+ for (const path of forbiddenSourceFiles) {
27
+ errors.push({
28
+ code: "forbidden-source-file",
29
+ path,
30
+ message: "packages/cli/src 는 npm tarball에 포함되면 안 됩니다",
31
+ });
32
+ }
33
+
34
+ for (const path of requiredFiles) {
35
+ if (!fileSet.has(path)) {
36
+ errors.push({
37
+ code: "missing-required-file",
38
+ path,
39
+ message: "필수 CLI artifact가 npm tarball에 없습니다",
40
+ });
41
+ }
42
+ }
43
+
44
+ return {
45
+ ok: errors.length === 0,
46
+ errors,
47
+ };
48
+ }
49
+
50
+ export function validateGeneratedCodegenBundle(source) {
51
+ const errors = [];
52
+ for (const rule of FORBIDDEN_CODEGEN_CORE_IMPORTS) {
53
+ if (rule.pattern.test(source)) {
54
+ errors.push({
55
+ code: rule.id,
56
+ path: "lib/codegen/index.mjs",
57
+ message: rule.message,
58
+ });
59
+ }
60
+ }
61
+ return {
62
+ ok: errors.length === 0,
63
+ errors,
64
+ };
65
+ }
66
+
67
+ export function formatArtifactGuardErrors(errors) {
68
+ return errors.map((error) => ` ❌ ${error.path} — ${error.message}`).join("\n");
69
+ }
@@ -0,0 +1,146 @@
1
+ import { execFileSync } from "child_process";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
3
+ import { homedir } from "os";
4
+ import { dirname, resolve } from "path";
5
+
6
+ const DEFAULT_CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
7
+ const DEFAULT_NPM_TIMEOUT_MS = 1500;
8
+
9
+ function isEnabled(value) {
10
+ return value === "1" || value === "true" || value === "yes";
11
+ }
12
+
13
+ export function parseCliSemver(version) {
14
+ const match = String(version ?? "").trim().match(/^v?(\d+)\.(\d+)\.(\d+)/);
15
+ if (!match) return null;
16
+ return {
17
+ major: Number(match[1]),
18
+ minor: Number(match[2]),
19
+ patch: Number(match[3]),
20
+ };
21
+ }
22
+
23
+ export function compareCliSemver(left, right) {
24
+ const a = typeof left === "string" ? parseCliSemver(left) : left;
25
+ const b = typeof right === "string" ? parseCliSemver(right) : right;
26
+ if (!a || !b) return 0;
27
+ for (const key of ["major", "minor", "patch"]) {
28
+ if (a[key] > b[key]) return 1;
29
+ if (a[key] < b[key]) return -1;
30
+ }
31
+ return 0;
32
+ }
33
+
34
+ export function shouldWarnForStaleCli({ currentVersion, latestVersion }) {
35
+ return compareCliSemver(latestVersion, currentVersion) > 0;
36
+ }
37
+
38
+ export function resolveVersionCheckCachePath({ homeDir = homedir(), processEnv = process.env } = {}) {
39
+ return processEnv.GENCOW_VERSION_CHECK_CACHE || resolve(homeDir, ".gencow", "version-check.json");
40
+ }
41
+
42
+ function readCache(cachePath, { existsSyncImpl = existsSync, readFileSyncImpl = readFileSync } = {}) {
43
+ try {
44
+ if (!existsSyncImpl(cachePath)) return {};
45
+ return JSON.parse(readFileSyncImpl(cachePath, "utf8"));
46
+ } catch {
47
+ return {};
48
+ }
49
+ }
50
+
51
+ function writeCache(cachePath, cache, { mkdirSyncImpl = mkdirSync, writeFileSyncImpl = writeFileSync } = {}) {
52
+ try {
53
+ mkdirSyncImpl(dirname(cachePath), { recursive: true });
54
+ writeFileSyncImpl(cachePath, JSON.stringify(cache, null, 2) + "\n");
55
+ } catch {
56
+ /* version check cache must never break CLI commands */
57
+ }
58
+ }
59
+
60
+ function isFresh(timestamp, now, maxAgeMs) {
61
+ return typeof timestamp === "number" && now - timestamp >= 0 && now - timestamp < maxAgeMs;
62
+ }
63
+
64
+ function fetchLatestVersion({ execFileSyncImpl = execFileSync, timeoutMs = DEFAULT_NPM_TIMEOUT_MS } = {}) {
65
+ return String(
66
+ execFileSyncImpl("npm", ["view", "gencow", "version", "--silent"], {
67
+ encoding: "utf8",
68
+ stdio: ["ignore", "pipe", "ignore"],
69
+ timeout: timeoutMs,
70
+ }),
71
+ ).trim();
72
+ }
73
+
74
+ export function formatStaleCliWarning({ currentVersion, latestVersion, command }) {
75
+ const suffix = command ? ` ${command}` : "";
76
+ return [
77
+ `Gencow CLI is older than npm latest (current ${currentVersion}, latest ${latestVersion}).`,
78
+ `Use project-local/latest CLI for verification: bunx gencow@latest${suffix}`,
79
+ "If this came from a global install, check: which gencow && gencow --version",
80
+ "Skip this check with GENCOW_SKIP_VERSION_CHECK=true.",
81
+ ].join("\n");
82
+ }
83
+
84
+ export function maybeCheckCliVersion({
85
+ cacheMaxAgeMs = DEFAULT_CACHE_MAX_AGE_MS,
86
+ command,
87
+ currentVersion,
88
+ errorImpl = console.error,
89
+ execFileSyncImpl = execFileSync,
90
+ exitImpl = (code) => process.exit(code),
91
+ existsSyncImpl = existsSync,
92
+ homeDirImpl = homedir,
93
+ mkdirSyncImpl = mkdirSync,
94
+ nowImpl = () => Date.now(),
95
+ processEnv = process.env,
96
+ readFileSyncImpl = readFileSync,
97
+ warnImpl = console.warn,
98
+ writeFileSyncImpl = writeFileSync,
99
+ } = {}) {
100
+ if (isEnabled(processEnv.GENCOW_SKIP_VERSION_CHECK)) {
101
+ return { status: "skipped" };
102
+ }
103
+ if (command === "help" || command === "version") {
104
+ return { status: "skipped" };
105
+ }
106
+
107
+ const requireLatest = isEnabled(processEnv.GENCOW_REQUIRE_LATEST_CLI);
108
+ const now = nowImpl();
109
+ const cachePath = resolveVersionCheckCachePath({ homeDir: homeDirImpl(), processEnv });
110
+ const cache = readCache(cachePath, { existsSyncImpl, readFileSyncImpl });
111
+ let latestVersion = cache.latestVersion;
112
+
113
+ if (!latestVersion || !isFresh(cache.checkedAt, now, cacheMaxAgeMs)) {
114
+ try {
115
+ latestVersion = fetchLatestVersion({ execFileSyncImpl });
116
+ cache.latestVersion = latestVersion;
117
+ cache.checkedAt = now;
118
+ writeCache(cachePath, cache, { mkdirSyncImpl, writeFileSyncImpl });
119
+ } catch (caught) {
120
+ return { status: "unverified", error: caught };
121
+ }
122
+ }
123
+
124
+ if (!shouldWarnForStaleCli({ currentVersion, latestVersion })) {
125
+ return { status: "fresh", latestVersion };
126
+ }
127
+
128
+ const message = formatStaleCliWarning({ currentVersion, latestVersion, command });
129
+ if (requireLatest) {
130
+ errorImpl(message);
131
+ exitImpl(1);
132
+ return { status: "blocked", latestVersion };
133
+ }
134
+
135
+ const warnedRecently =
136
+ cache.warnedForVersion === latestVersion && isFresh(cache.warnedAt, now, cacheMaxAgeMs);
137
+ if (!warnedRecently) {
138
+ warnImpl(message);
139
+ cache.warnedAt = now;
140
+ cache.warnedForVersion = latestVersion;
141
+ writeCache(cachePath, cache, { mkdirSyncImpl, writeFileSyncImpl });
142
+ return { status: "warned", latestVersion };
143
+ }
144
+
145
+ return { status: "stale-suppressed", latestVersion };
146
+ }
@@ -13,9 +13,13 @@ export const INIT_TEMPLATES = [
13
13
  {
14
14
  id: "fullstack",
15
15
  label: "Tasks + Files + AI Chat + Agent 백엔드",
16
- deps: { ai: "^4.0.0", "@ai-sdk/openai": "^1.0.0" },
16
+ deps: { ai: "^5.0.0", "@ai-sdk/openai": "^2.0.0", "@ai-sdk/provider-utils": "^3.0.0" },
17
+ },
18
+ {
19
+ id: "ai-chat",
20
+ label: "AI 챗봇 + Memory 백엔드",
21
+ deps: { ai: "^5.0.0", "@ai-sdk/openai": "^2.0.0", "@ai-sdk/provider-utils": "^3.0.0" },
17
22
  },
18
- { id: "ai-chat", label: "AI 챗봇 + Memory 백엔드", deps: { ai: "^4.0.0", "@ai-sdk/openai": "^1.0.0" } },
19
23
  ];
20
24
 
21
25
  const BASE_DEPS = {
@@ -30,11 +34,19 @@ const BASE_DEPS = {
30
34
  };
31
35
 
32
36
  const DEFAULT_SCRIPTS = {
37
+ codegen: "gencow codegen",
33
38
  dev: "gencow dev",
39
+ typecheck: "tsc --noEmit",
40
+ build: "tsc -p tsconfig.json",
34
41
  "db:push": "gencow db:push",
35
42
  "db:generate": "gencow db:generate",
36
43
  };
37
44
 
45
+ const DEFAULT_DEV_DEPS = {
46
+ "@types/node": "^20.0.0",
47
+ typescript: "^5.0.0",
48
+ };
49
+
38
50
  const GENERATED_CODEGEN_ARTIFACTS = ["api.ts", "db.d.ts", "operations.d.ts", "zod.ts"];
39
51
  const CODEGEN_ARTIFACT_DIRS = ["src/gencow", "gencow"];
40
52
 
@@ -127,6 +139,7 @@ export function parseInitArgs(initArgs) {
127
139
  force: false,
128
140
  help: false,
129
141
  name: null,
142
+ noInstall: false,
130
143
  templateId: null,
131
144
  };
132
145
 
@@ -138,6 +151,8 @@ export function parseInitArgs(initArgs) {
138
151
  parsed.templateId = initArgs[++i] ?? null;
139
152
  } else if (arg === "--force" || arg === "-f") {
140
153
  parsed.force = true;
154
+ } else if (arg === "--no-install") {
155
+ parsed.noInstall = true;
141
156
  } else if (!arg.startsWith("-")) {
142
157
  parsed.name = arg;
143
158
  }
@@ -160,6 +175,10 @@ export function buildInitPackageJson({ name, templateDeps = {}, existingPackageJ
160
175
  ...BASE_DEPS,
161
176
  ...templateDeps,
162
177
  };
178
+ pkg.devDependencies = {
179
+ ...(existingPackageJson?.devDependencies || {}),
180
+ ...DEFAULT_DEV_DEPS,
181
+ };
163
182
  pkg.scripts = { ...(existingPackageJson?.scripts || {}) };
164
183
 
165
184
  for (const [key, value] of Object.entries(DEFAULT_SCRIPTS)) {
@@ -194,7 +213,8 @@ function renderInitHelp(logImpl) {
194
213
  logImpl(` ${CYAN}name${RESET} Project name (creates directory) or "." for current dir\n`);
195
214
  logImpl(` ${BOLD}Options:${RESET}`);
196
215
  logImpl(` ${DIM}--template, -t${RESET} Select template (default, task-app, fullstack, ai-chat)`);
197
- logImpl(` ${DIM}--force, -f${RESET} Initialize in non-empty directory\n`);
216
+ logImpl(` ${DIM}--force, -f${RESET} Initialize in non-empty directory`);
217
+ logImpl(` ${DIM}--no-install${RESET} Skip dependency install (CI/canary)\n`);
198
218
  logImpl(` ${BOLD}Examples:${RESET}`);
199
219
  logImpl(` gencow init my-app`);
200
220
  logImpl(` gencow init . --force`);
@@ -268,7 +288,7 @@ export function createInitCommand({
268
288
  return;
269
289
  }
270
290
 
271
- let { name, templateId, force } = parsed;
291
+ let { name, templateId, force, noInstall } = parsed;
272
292
  if (!name) {
273
293
  name = await promptQuestion(`\n ${DIM}Enter project name (current folder: .):${RESET} `, {
274
294
  createInterfaceImpl,
@@ -493,16 +513,20 @@ export function createInitCommand({
493
513
  }
494
514
  successImpl("Linked @gencow/core + drizzle-orm + hono (when resolvable)");
495
515
 
496
- infoImpl("Installing dependencies...");
497
- try {
498
- execSyncImpl("bun install", { cwd: projectDir, stdio: ["ignore", "pipe", "pipe"] });
499
- successImpl("Dependencies installed");
500
- } catch {
501
- warnImpl(
502
- "Package install failed — " +
503
- (isCwd ? "bun install" : "cd " + name + " && bun install") +
504
- " to install manually",
505
- );
516
+ if (noInstall) {
517
+ infoImpl("Skipping dependency install (--no-install).");
518
+ } else {
519
+ infoImpl("Installing dependencies...");
520
+ try {
521
+ execSyncImpl("bun install", { cwd: projectDir, stdio: ["ignore", "pipe", "pipe"] });
522
+ successImpl("Dependencies installed");
523
+ } catch {
524
+ warnImpl(
525
+ "Package install failed — " +
526
+ (isCwd ? "bun install" : "cd " + name + " && bun install") +
527
+ " to install manually",
528
+ );
529
+ }
506
530
  }
507
531
 
508
532
  logImpl(renderNextSteps({ isCwd, name, templateId: template.id }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gencow",
3
- "version": "0.1.146",
3
+ "version": "0.1.147",
4
4
  "description": "Gencow — AI Backend Engine",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,20 +11,18 @@ import { execSync } from "child_process";
11
11
  import { readFileSync, existsSync } from "fs";
12
12
  import { resolve, dirname, relative, join } from "path";
13
13
  import { fileURLToPath } from "url";
14
+ import {
15
+ REQUIRED_CLI_TARBALL_FILES,
16
+ formatArtifactGuardErrors,
17
+ validateCliTarballFiles,
18
+ validateGeneratedCodegenBundle,
19
+ } from "../lib/cli-artifact-guard.mjs";
14
20
 
15
21
  const __dirname = dirname(fileURLToPath(import.meta.url));
16
22
  const CLI_ROOT = resolve(__dirname, "..");
17
23
 
18
24
  // ── 1. 필수 파일 목록 (반드시 tarball에 포함되어야 함) ─────
19
- const REQUIRED_FILES = [
20
- "bin/gencow.mjs",
21
- "bin/gencow-mcp.mjs",
22
- "lib/readme-codegen.mjs",
23
- "lib/deploy-auditor.mjs",
24
- "lib/codegen/index.mjs",
25
- "server/index.js",
26
- "core/index.js",
27
- ];
25
+ const REQUIRED_FILES = REQUIRED_CLI_TARBALL_FILES;
28
26
 
29
27
  // ── 2. gencow.mjs에서 import 경로 자동 추출 ───────────────
30
28
  function extractImports(entryFile) {
@@ -67,9 +65,9 @@ function checkGeneratedCodegenBundle() {
67
65
  return false;
68
66
  }
69
67
 
70
- const bundledCodegen = readFileSync(codegenBundlePath, "utf8");
71
- if (/collectProcedureRouteMaps[\s\S]{0,240}from "@gencow\/core"/.test(bundledCodegen)) {
72
- console.error(" ❌ lib/codegen/index.mjs — @gencow/core에서 server-only procedure route collector를 import합니다");
68
+ const validation = validateGeneratedCodegenBundle(readFileSync(codegenBundlePath, "utf8"));
69
+ if (!validation.ok) {
70
+ console.error(formatArtifactGuardErrors(validation.errors));
73
71
  console.error(" packages/cli/src/codegen/frontend/registry-extractor.ts를 확인한 뒤 CLI build를 다시 실행하세요");
74
72
  return false;
75
73
  }
@@ -94,13 +92,14 @@ function main() {
94
92
  }
95
93
 
96
94
  const tarballSet = new Set(tarballFiles);
95
+ const tarballValidation = validateCliTarballFiles(tarballFiles, REQUIRED_FILES);
97
96
 
98
97
  // 4-1b. TypeScript 소스(src/)는 번들 산출물만 배포 — tarball에 포함되면 실패
99
98
  console.log(" ── 소스 디렉터리 제외 확인 ───────────────");
100
- const forbiddenSource = tarballFiles.filter((p) => p === "src" || p.startsWith("src/"));
99
+ const forbiddenSource = tarballValidation.errors.filter((item) => item.code === "forbidden-source-file");
101
100
  if (forbiddenSource.length > 0) {
102
- for (const p of forbiddenSource) {
103
- console.error(` ❌ ${p} — packages/cli/src 는 npm 에 포함되면 안 됩니다 (lib/ 번들만 배포)`);
101
+ for (const item of forbiddenSource) {
102
+ console.error(` ❌ ${item.path} — ${item.message} (lib/ 번들만 배포)`);
104
103
  }
105
104
  hasError = true;
106
105
  } else {
@@ -8,14 +8,13 @@ import {
8
8
  embed as aiEmbed,
9
9
  embedMany as aiEmbedMany,
10
10
  generateObject as aiGenerateObject,
11
- type FlexibleSchema,
12
11
  type GenerateObjectResult,
13
- type InferSchema,
14
12
  type LanguageModel,
15
13
  type LanguageModelUsage,
16
14
  type ModelMessage,
17
15
  type ToolSet,
18
16
  } from "ai";
17
+ import type { FlexibleSchema, InferSchema } from "@ai-sdk/provider-utils";
19
18
  import { createOpenAI } from "@ai-sdk/openai";
20
19
 
21
20
  // ── Gencow AI 자동 전환 ─────────────────────────────────
@@ -14,6 +14,11 @@ import { query, mutation, v } from "@gencow/core";
14
14
  import { ai } from "./ai";
15
15
  import { conversations, messages } from "./schema";
16
16
 
17
+ type ChatMessageRow = {
18
+ role: string;
19
+ content: string;
20
+ };
21
+
17
22
  // ─── Queries ─────────────────────────────────────────────
18
23
 
19
24
  export const listConversations = query("chat.listConversations", {
@@ -31,18 +36,20 @@ export const getMessages = query("chat.getMessages", {
31
36
  args: { conversationId: v.number() },
32
37
  handler: async (ctx, args) => {
33
38
  const user = ctx.auth.requireAuth(); // 🔒 인증 필수
39
+ const conversationId = args.conversationId;
40
+ if (typeof conversationId !== "number") return [];
34
41
 
35
42
  // 🔒 대화 소유권 확인 (다른 사람 대화 메시지 조회 차단)
36
43
  const conv = await ctx.db
37
44
  .select()
38
45
  .from(conversations)
39
- .where(and(eq(conversations.id, args.conversationId), eq(conversations.userId, user.id)));
46
+ .where(and(eq(conversations.id, conversationId), eq(conversations.userId, user.id)));
40
47
  if (conv.length === 0) return [];
41
48
 
42
49
  return await ctx.db
43
50
  .select()
44
51
  .from(messages)
45
- .where(eq(messages.conversationId, args.conversationId))
52
+ .where(eq(messages.conversationId, conversationId))
46
53
  .orderBy(messages.createdAt);
47
54
  },
48
55
  });
@@ -57,6 +64,11 @@ export const send = mutation({
57
64
  },
58
65
  handler: async (ctx, args) => {
59
66
  const user = ctx.auth.requireAuth(); // 🔒 인증 필수
67
+ const userMessage = args.message;
68
+ if (typeof userMessage !== "string" || userMessage.trim().length === 0) {
69
+ throw new Error("Message is required");
70
+ }
71
+
60
72
  let convId = args.conversationId;
61
73
 
62
74
  // 새 대화 자동 생성
@@ -64,10 +76,11 @@ export const send = mutation({
64
76
  const [conv] = await ctx.db
65
77
  .insert(conversations)
66
78
  .values({
67
- title: args.message.slice(0, 50),
79
+ title: userMessage.slice(0, 50),
68
80
  userId: user.id, // 🔒 소유자 기록
69
81
  })
70
82
  .returning();
83
+ if (!conv?.id) throw new Error("Conversation creation failed");
71
84
  convId = conv.id;
72
85
  } else {
73
86
  // 🔒 기존 대화 소유권 확인
@@ -79,12 +92,13 @@ export const send = mutation({
79
92
  throw new Error("Conversation not found");
80
93
  }
81
94
  }
95
+ if (typeof convId !== "number") throw new Error("Conversation not found");
82
96
 
83
97
  // 사용자 메시지 저장
84
98
  await ctx.db.insert(messages).values({
85
99
  conversationId: convId,
86
100
  role: "user",
87
- content: args.message,
101
+ content: userMessage,
88
102
  });
89
103
 
90
104
  // 대화 히스토리 로드
@@ -99,7 +113,7 @@ export const send = mutation({
99
113
  model: "gpt-4o-mini",
100
114
  messages: [
101
115
  { role: "system", content: "You are a helpful assistant. 한국어로 답변해주세요." },
102
- ...history.map((m) => ({ role: m.role, content: m.content })),
116
+ ...(history as ChatMessageRow[]).map((m: ChatMessageRow) => ({ role: m.role, content: m.content })),
103
117
  ],
104
118
  });
105
119
 
package/templates/ai.ts CHANGED
@@ -8,14 +8,13 @@ import {
8
8
  embed as aiEmbed,
9
9
  embedMany as aiEmbedMany,
10
10
  generateObject as aiGenerateObject,
11
- type FlexibleSchema,
12
11
  type GenerateObjectResult,
13
- type InferSchema,
14
12
  type LanguageModel,
15
13
  type LanguageModelUsage,
16
14
  type ModelMessage,
17
15
  type ToolSet,
18
16
  } from "ai";
17
+ import type { FlexibleSchema, InferSchema } from "@ai-sdk/provider-utils";
19
18
  import { createOpenAI } from "@ai-sdk/openai";
20
19
 
21
20
  // ── Gencow AI 자동 전환 ─────────────────────────────────
@@ -8,14 +8,13 @@ import {
8
8
  embed as aiEmbed,
9
9
  embedMany as aiEmbedMany,
10
10
  generateObject as aiGenerateObject,
11
- type FlexibleSchema,
12
11
  type GenerateObjectResult,
13
- type InferSchema,
14
12
  type LanguageModel,
15
13
  type LanguageModelUsage,
16
14
  type ModelMessage,
17
15
  type ToolSet,
18
16
  } from "ai";
17
+ import type { FlexibleSchema, InferSchema } from "@ai-sdk/provider-utils";
19
18
  import { createOpenAI } from "@ai-sdk/openai";
20
19
 
21
20
  // ── Gencow AI 자동 전환 ─────────────────────────────────