@covro/covro 0.0.0-stage → 0.1.0-dev.1

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 (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +173 -2
  3. package/README.zh-CN.md +174 -0
  4. package/bin/covro.mjs +8 -0
  5. package/dist/cli/control.js +66 -0
  6. package/dist/cli/control.js.map +7 -0
  7. package/dist/cli/daemon.js +118 -0
  8. package/dist/cli/daemon.js.map +7 -0
  9. package/dist/cli/main.js +152 -0
  10. package/dist/cli/main.js.map +7 -0
  11. package/dist/cli/paths.js +60 -0
  12. package/dist/cli/paths.js.map +7 -0
  13. package/dist/cli/service.js +87 -0
  14. package/dist/cli/service.js.map +7 -0
  15. package/dist/codex/adapter.js +1195 -0
  16. package/dist/codex/adapter.js.map +7 -0
  17. package/dist/codex/auto-review.js +102 -0
  18. package/dist/codex/auto-review.js.map +7 -0
  19. package/dist/codex/compatibility.js +7 -0
  20. package/dist/codex/compatibility.js.map +7 -0
  21. package/dist/codex/mapping.js +210 -0
  22. package/dist/codex/mapping.js.map +7 -0
  23. package/dist/codex/permissions.js +51 -0
  24. package/dist/codex/permissions.js.map +7 -0
  25. package/dist/codex/thread-title-policy.js +52 -0
  26. package/dist/codex/thread-title-policy.js.map +7 -0
  27. package/dist/codex/thread-titles.js +259 -0
  28. package/dist/codex/thread-titles.js.map +7 -0
  29. package/dist/codex/transport.js +236 -0
  30. package/dist/codex/transport.js.map +7 -0
  31. package/dist/codex/turn-token-usage.js +46 -0
  32. package/dist/codex/turn-token-usage.js.map +7 -0
  33. package/dist/codex/types.js +1 -0
  34. package/dist/codex/types.js.map +7 -0
  35. package/dist/drizzle/0000_past_wildside.sql +45 -0
  36. package/dist/drizzle/0001_shocking_ender_wiggin.sql +2 -0
  37. package/dist/drizzle/0002_allow_parallel_threads.sql +1 -0
  38. package/dist/drizzle/0003_project_registrations.sql +11 -0
  39. package/dist/drizzle/0004_turn_usage.sql +6 -0
  40. package/dist/drizzle/meta/0000_snapshot.json +309 -0
  41. package/dist/drizzle/meta/0001_snapshot.json +323 -0
  42. package/dist/drizzle/meta/0002_snapshot.json +315 -0
  43. package/dist/drizzle/meta/0003_snapshot.json +381 -0
  44. package/dist/drizzle/meta/0004_snapshot.json +420 -0
  45. package/dist/drizzle/meta/_journal.json +41 -0
  46. package/dist/server/auth.js +47 -0
  47. package/dist/server/auth.js.map +7 -0
  48. package/dist/server/compaction.js +40 -0
  49. package/dist/server/compaction.js.map +7 -0
  50. package/dist/server/config-store.js +147 -0
  51. package/dist/server/config-store.js.map +7 -0
  52. package/dist/server/config.js +124 -0
  53. package/dist/server/config.js.map +7 -0
  54. package/dist/server/db/index.js +21 -0
  55. package/dist/server/db/index.js.map +7 -0
  56. package/dist/server/db/migrate.js +26 -0
  57. package/dist/server/db/migrate.js.map +7 -0
  58. package/dist/server/db/operations.js +201 -0
  59. package/dist/server/db/operations.js.map +7 -0
  60. package/dist/server/db/schema.js +99 -0
  61. package/dist/server/db/schema.js.map +7 -0
  62. package/dist/server/db/turn-usage.js +31 -0
  63. package/dist/server/db/turn-usage.js.map +7 -0
  64. package/dist/server/events.js +123 -0
  65. package/dist/server/events.js.map +7 -0
  66. package/dist/server/http.js +305 -0
  67. package/dist/server/http.js.map +7 -0
  68. package/dist/server/images.js +49 -0
  69. package/dist/server/images.js.map +7 -0
  70. package/dist/server/instance-lock.js +25 -0
  71. package/dist/server/instance-lock.js.map +7 -0
  72. package/dist/server/local-files.js +100 -0
  73. package/dist/server/local-files.js.map +7 -0
  74. package/dist/server/main.js +125 -0
  75. package/dist/server/main.js.map +7 -0
  76. package/dist/server/markdown.js +28 -0
  77. package/dist/server/markdown.js.map +7 -0
  78. package/dist/server/projection.js +348 -0
  79. package/dist/server/projection.js.map +7 -0
  80. package/dist/server/projects.js +230 -0
  81. package/dist/server/projects.js.map +7 -0
  82. package/dist/server/service.js +1221 -0
  83. package/dist/server/service.js.map +7 -0
  84. package/dist/server/snapshot.js +98 -0
  85. package/dist/server/snapshot.js.map +7 -0
  86. package/dist/server/thread-export.js +97 -0
  87. package/dist/server/thread-export.js.map +7 -0
  88. package/dist/server/websocket.js +126 -0
  89. package/dist/server/websocket.js.map +7 -0
  90. package/dist/shared/async-questions.js +39 -0
  91. package/dist/shared/async-questions.js.map +7 -0
  92. package/dist/shared/codex-compatibility.js +56 -0
  93. package/dist/shared/codex-compatibility.js.map +7 -0
  94. package/dist/shared/elicitation.js +314 -0
  95. package/dist/shared/elicitation.js.map +7 -0
  96. package/dist/shared/errors.js +24 -0
  97. package/dist/shared/errors.js.map +7 -0
  98. package/dist/shared/gateway-config.js +1 -0
  99. package/dist/shared/gateway-config.js.map +7 -0
  100. package/dist/shared/images.js +21 -0
  101. package/dist/shared/images.js.map +7 -0
  102. package/dist/shared/package.js +5 -0
  103. package/dist/shared/package.js.map +7 -0
  104. package/dist/shared/permissions.js +63 -0
  105. package/dist/shared/permissions.js.map +7 -0
  106. package/dist/shared/thread-name.js +14 -0
  107. package/dist/shared/thread-name.js.map +7 -0
  108. package/dist/shared/thread-summary.js +24 -0
  109. package/dist/shared/thread-summary.js.map +7 -0
  110. package/dist/shared/types.js +1 -0
  111. package/dist/shared/types.js.map +7 -0
  112. package/dist/shared/updates.js +7 -0
  113. package/dist/shared/updates.js.map +7 -0
  114. package/dist/shared/validation.js +116 -0
  115. package/dist/shared/validation.js.map +7 -0
  116. package/dist/shared/versions.js +36 -0
  117. package/dist/shared/versions.js.map +7 -0
  118. package/dist/update-worker.cjs +21968 -0
  119. package/dist/updates/files.js +54 -0
  120. package/dist/updates/files.js.map +7 -0
  121. package/dist/updates/manager.js +315 -0
  122. package/dist/updates/manager.js.map +7 -0
  123. package/dist/updates/registry.js +71 -0
  124. package/dist/updates/registry.js.map +7 -0
  125. package/dist/updates/transaction.js +96 -0
  126. package/dist/updates/transaction.js.map +7 -0
  127. package/dist/updates/worker.js +345 -0
  128. package/dist/updates/worker.js.map +7 -0
  129. package/dist/web/assets/index-BW9afH_Y.js +83 -0
  130. package/dist/web/assets/index-L3S1ZiO0.css +1 -0
  131. package/dist/web/covro.svg +6 -0
  132. package/dist/web/index.html +15 -0
  133. package/package.json +88 -4
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/server/db/operations.ts"],
4
+ "sourcesContent": ["import { createHash } from 'node:crypto';\nimport { and, eq, isNull, or } from 'drizzle-orm';\nimport type { AppDatabase } from './index.js';\nimport { executionMarkers, operations, threadLinks } from './schema.js';\nimport type { ApiError } from '../../shared/types.js';\n\nexport type OperationRow = typeof operations.$inferSelect;\nexport type ExecutionMarker = typeof executionMarkers.$inferSelect;\nexport type ThreadLink = typeof threadLinks.$inferSelect;\n\nexport class StoreConflict extends Error {\n constructor(\n public readonly code: 'OPERATION_CONFLICT' | 'THREAD_BUSY',\n message: string,\n public readonly threadId?: string,\n ) {\n super(message);\n }\n}\n\nfunction canonical(value: unknown): string {\n if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;\n if (value !== null && typeof value === 'object') {\n return `{${Object.entries(value)\n .filter(([, item]) => item !== undefined)\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, item]) => `${JSON.stringify(key)}:${canonical(item)}`)\n .join(',')}}`;\n }\n return JSON.stringify(value) ?? 'null';\n}\n\nexport function requestDigest(value: unknown): string {\n return createHash('sha256').update(canonical(value)).digest('hex');\n}\n\n/** Every transaction is synchronous; no process/network work occurs inside one. */\nexport class OperationStore {\n constructor(public readonly database: AppDatabase) {}\n\n accept(\n clientOperationId: string,\n kind: string,\n target: string,\n request: unknown,\n ): { operation: OperationRow; inserted: boolean } {\n const digest = requestDigest(request);\n return this.database.transaction((tx) => {\n const now = Date.now();\n const inserted =\n tx\n .insert(operations)\n .values({\n clientOperationId,\n kind,\n target,\n requestDigest: digest,\n state: 'accepted',\n createdAt: now,\n updatedAt: now,\n })\n .onConflictDoNothing({ target: operations.clientOperationId })\n .run().changes === 1;\n const operation = tx\n .select()\n .from(operations)\n .where(eq(operations.clientOperationId, clientOperationId))\n .get();\n if (!operation) throw new Error('Operation insert did not persist');\n if (\n operation.kind !== kind ||\n operation.target !== target ||\n operation.requestDigest !== digest\n ) {\n throw new StoreConflict('OPERATION_CONFLICT', '\u540C\u4E00\u64CD\u4F5C ID \u5DF2\u7528\u4E8E\u4E0D\u540C\u7684\u8BF7\u6C42');\n }\n return { operation, inserted };\n });\n }\n\n get(id: string): OperationRow | undefined {\n return this.database\n .select()\n .from(operations)\n .where(eq(operations.clientOperationId, id))\n .get();\n }\n\n associateProject(id: string, projectId: string): void {\n this.database\n .update(operations)\n .set({ projectId })\n .where(and(eq(operations.clientOperationId, id), eq(operations.state, 'accepted')))\n .run();\n }\n\n /** Includes older rows whose project can be recovered from their stable target/link. */\n pendingProjectOperations(projectId: string): OperationRow[] {\n return this.database\n .select()\n .from(operations)\n .where(\n or(\n eq(operations.state, 'accepted'),\n eq(operations.state, 'dispatching'),\n and(eq(operations.state, 'unknown'), isNull(operations.reconciledAt)),\n ),\n )\n .all()\n .filter((operation) => {\n if (\n ![\n 'thread.create',\n 'thread.resume',\n 'thread.fork',\n 'thread.archive',\n 'thread.rename',\n ].includes(operation.kind)\n )\n return false;\n const owner =\n operation.projectId ??\n (operation.kind === 'thread.create'\n ? operation.target\n : this.getThreadLink(operation.target)?.projectId);\n return owner === projectId;\n });\n }\n\n dispatch(\n id: string,\n marker?: { threadId: string; workspaceKey: string; codexEpoch: string },\n ): void {\n this.database.transaction((tx) => {\n if (marker) {\n const occupied = tx\n .select()\n .from(executionMarkers)\n .where(\n and(isNull(executionMarkers.endedAt), eq(executionMarkers.threadId, marker.threadId)),\n )\n .get();\n if (occupied)\n throw new StoreConflict(\n 'THREAD_BUSY',\n '\u8BE5\u4F1A\u8BDD\u7684\u4E0A\u4E00\u8F6E\u6267\u884C\u5C1A\u672A\u7ED3\u675F\uFF0C\u8BF7\u8FFD\u52A0\u8981\u6C42\u6216\u7B49\u5F85\u7ED3\u675F',\n occupied.threadId,\n );\n }\n const updated = tx\n .update(operations)\n .set({ state: 'dispatching', updatedAt: Date.now() })\n .where(and(eq(operations.clientOperationId, id), eq(operations.state, 'accepted')))\n .run();\n if (updated.changes !== 1)\n throw new StoreConflict('OPERATION_CONFLICT', '\u64CD\u4F5C\u5DF2\u7ECF\u6D3E\u53D1\u6216\u7ED3\u675F');\n if (marker)\n tx.insert(executionMarkers)\n .values({ operationId: id, ...marker, state: 'active', createdAt: Date.now() })\n .run();\n });\n }\n\n acknowledge(id: string, result: unknown, turnId?: string): OperationRow {\n this.database.transaction((tx) => {\n tx.update(operations)\n .set({ state: 'acknowledged', result, error: null, updatedAt: Date.now() })\n .where(and(eq(operations.clientOperationId, id), eq(operations.state, 'dispatching')))\n .run();\n if (turnId !== undefined)\n tx.update(executionMarkers)\n .set({ turnId })\n .where(eq(executionMarkers.operationId, id))\n .run();\n });\n return this.get(id)!;\n }\n\n fail(id: string, error: ApiError, uncertain = false): OperationRow {\n this.database.transaction((tx) => {\n const changed = tx\n .update(operations)\n .set({ state: uncertain ? 'unknown' : 'failed', error, updatedAt: Date.now() })\n .where(\n and(\n eq(operations.clientOperationId, id),\n or(eq(operations.state, 'accepted'), eq(operations.state, 'dispatching')),\n ),\n )\n .run();\n // A late rejection must not turn an already-recovered unknown execution into a release.\n if (changed.changes === 0) return;\n tx.update(executionMarkers)\n .set(uncertain ? { state: 'unknown' } : { state: 'terminal', endedAt: Date.now() })\n .where(and(eq(executionMarkers.operationId, id), isNull(executionMarkers.endedAt)))\n .run();\n });\n return this.get(id)!;\n }\n\n /** Includes acknowledged requests with unfinished turns; none of these are retried. */\n recover(): void {\n this.database.transaction((tx) => {\n tx.update(operations)\n .set({\n state: 'failed',\n error: { code: 'NOT_DISPATCHED', message: '\u670D\u52A1\u5728\u6D3E\u53D1\u524D\u505C\u6B62\uFF1B\u64CD\u4F5C\u6CA1\u6709\u6267\u884C' },\n updatedAt: Date.now(),\n })\n .where(eq(operations.state, 'accepted'))\n .run();\n tx.update(operations)\n .set({\n state: 'unknown',\n error: { code: 'RESULT_UNKNOWN', message: '\u8FD0\u884C\u65F6\u5DF2\u505C\u6B62\uFF0C\u63D0\u4EA4\u7ED3\u679C\u9700\u8981\u6838\u5BF9' },\n updatedAt: Date.now(),\n })\n .where(eq(operations.state, 'dispatching'))\n .run();\n tx.update(executionMarkers)\n .set({ state: 'unknown' })\n .where(isNull(executionMarkers.endedAt))\n .run();\n });\n }\n\n activeMarkers(): ExecutionMarker[] {\n return this.database\n .select()\n .from(executionMarkers)\n .where(isNull(executionMarkers.endedAt))\n .all();\n }\n\n unresolvedOperations(): OperationRow[] {\n return this.database\n .select()\n .from(operations)\n .where(\n or(\n eq(operations.state, 'accepted'),\n eq(operations.state, 'dispatching'),\n and(eq(operations.state, 'unknown'), isNull(operations.reconciledAt)),\n ),\n )\n .all();\n }\n\n marker(operationId: string): ExecutionMarker | undefined {\n return this.database\n .select()\n .from(executionMarkers)\n .where(eq(executionMarkers.operationId, operationId))\n .get();\n }\n\n activeForThread(threadId: string): ExecutionMarker | undefined {\n return this.database\n .select()\n .from(executionMarkers)\n .where(and(eq(executionMarkers.threadId, threadId), isNull(executionMarkers.endedAt)))\n .get();\n }\n\n attachTurn(operationId: string, turnId: string): void {\n this.database\n .update(executionMarkers)\n .set({ turnId })\n .where(and(eq(executionMarkers.operationId, operationId), isNull(executionMarkers.endedAt)))\n .run();\n }\n\n finishTurn(threadId: string, turnId: string, codexEpoch: string): void {\n this.database\n .update(executionMarkers)\n .set({ state: 'terminal', endedAt: Date.now() })\n .where(\n and(\n eq(executionMarkers.threadId, threadId),\n eq(executionMarkers.turnId, turnId),\n eq(executionMarkers.codexEpoch, codexEpoch),\n isNull(executionMarkers.endedAt),\n ),\n )\n .run();\n }\n\n /** Keys the release to the original execution; an old decision cannot unlock a later turn. */\n reconcile(targetOperationId: string, reconciliationOperationId: string): void {\n this.database.transaction((tx) => {\n const now = Date.now();\n tx.update(operations)\n .set({ reconciledAt: now, reconciliationDecision: 'confirmed-ended' })\n .where(\n and(eq(operations.clientOperationId, targetOperationId), isNull(operations.reconciledAt)),\n )\n .run();\n tx.update(executionMarkers)\n .set({ state: 'reconciled', endedAt: now, reconciledAt: now, reconciliationOperationId })\n .where(\n and(\n eq(executionMarkers.operationId, targetOperationId),\n isNull(executionMarkers.endedAt),\n ),\n )\n .run();\n tx.update(operations)\n .set({\n state: 'acknowledged',\n result: { targetOperationId, decision: 'confirmed-ended', reconciledAt: now },\n updatedAt: now,\n })\n .where(\n and(\n eq(operations.clientOperationId, reconciliationOperationId),\n eq(operations.state, 'dispatching'),\n ),\n )\n .run();\n });\n }\n\n linkThread(link: Omit<ThreadLink, 'updatedAt'>): void {\n this.database\n .insert(threadLinks)\n .values({ ...link, updatedAt: Date.now() })\n .onConflictDoUpdate({ target: threadLinks.threadId, set: { ...link, updatedAt: Date.now() } })\n .run();\n }\n\n getThreadLink(threadId: string): ThreadLink | undefined {\n return this.database.select().from(threadLinks).where(eq(threadLinks.threadId, threadId)).get();\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,99 @@
1
+ import { sql } from "drizzle-orm";
2
+ import {
3
+ check,
4
+ index,
5
+ integer,
6
+ primaryKey,
7
+ sqliteTable,
8
+ text,
9
+ uniqueIndex
10
+ } from "drizzle-orm/sqlite-core";
11
+ const operations = sqliteTable(
12
+ "operations",
13
+ {
14
+ clientOperationId: text("client_operation_id").primaryKey(),
15
+ kind: text("kind").notNull(),
16
+ target: text("target").notNull(),
17
+ projectId: text("project_id"),
18
+ requestDigest: text("request_digest").notNull(),
19
+ state: text("state", {
20
+ enum: ["accepted", "dispatching", "acknowledged", "failed", "unknown"]
21
+ }).notNull(),
22
+ result: text("result", { mode: "json" }).$type(),
23
+ error: text("error", { mode: "json" }).$type(),
24
+ createdAt: integer("created_at").notNull(),
25
+ updatedAt: integer("updated_at").notNull(),
26
+ reconciledAt: integer("reconciled_at"),
27
+ reconciliationDecision: text("reconciliation_decision")
28
+ },
29
+ (table) => [
30
+ check(
31
+ "operations_state_check",
32
+ sql`${table.state} in ('accepted', 'dispatching', 'acknowledged', 'failed', 'unknown')`
33
+ ),
34
+ index("operations_state_idx").on(table.state)
35
+ ]
36
+ );
37
+ const executionMarkers = sqliteTable(
38
+ "execution_markers",
39
+ {
40
+ operationId: text("operation_id").primaryKey().references(() => operations.clientOperationId),
41
+ threadId: text("thread_id").notNull(),
42
+ turnId: text("turn_id"),
43
+ workspaceKey: text("workspace_key").notNull(),
44
+ codexEpoch: text("codex_epoch").notNull(),
45
+ state: text("state", { enum: ["active", "unknown", "terminal", "reconciled"] }).notNull(),
46
+ createdAt: integer("created_at").notNull(),
47
+ endedAt: integer("ended_at"),
48
+ reconciledAt: integer("reconciled_at"),
49
+ reconciliationOperationId: text("reconciliation_operation_id").references(
50
+ () => operations.clientOperationId
51
+ )
52
+ },
53
+ (table) => [
54
+ check(
55
+ "execution_markers_state_check",
56
+ sql`${table.state} in ('active', 'unknown', 'terminal', 'reconciled')`
57
+ ),
58
+ uniqueIndex("execution_markers_active_thread").on(table.threadId).where(sql`${table.endedAt} is null`)
59
+ ]
60
+ );
61
+ const threadLinks = sqliteTable("thread_links", {
62
+ threadId: text("thread_id").primaryKey(),
63
+ projectId: text("project_id").notNull(),
64
+ cwd: text("cwd").notNull(),
65
+ source: text("source").notNull(),
66
+ managedEpoch: text("managed_epoch"),
67
+ updatedAt: integer("updated_at").notNull()
68
+ });
69
+ const preferences = sqliteTable("preferences", {
70
+ key: text("key").primaryKey(),
71
+ value: text("value", { mode: "json" }).$type().notNull()
72
+ });
73
+ const turnUsage = sqliteTable(
74
+ "turn_usage",
75
+ {
76
+ threadId: text("thread_id").notNull(),
77
+ turnId: text("turn_id").notNull(),
78
+ outputTokens: integer("output_tokens").notNull()
79
+ },
80
+ (table) => [primaryKey({ columns: [table.threadId, table.turnId] })]
81
+ );
82
+ const projectRegistrations = sqliteTable("project_registrations", {
83
+ id: text("id").primaryKey(),
84
+ name: text("name").notNull(),
85
+ path: text("path").notNull(),
86
+ canonicalRoot: text("canonical_root"),
87
+ removedAt: integer("removed_at"),
88
+ createdAt: integer("created_at").notNull(),
89
+ updatedAt: integer("updated_at").notNull()
90
+ });
91
+ export {
92
+ executionMarkers,
93
+ operations,
94
+ preferences,
95
+ projectRegistrations,
96
+ threadLinks,
97
+ turnUsage
98
+ };
99
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/server/db/schema.ts"],
4
+ "sourcesContent": ["import { sql } from 'drizzle-orm';\nimport {\n check,\n index,\n integer,\n primaryKey,\n sqliteTable,\n text,\n uniqueIndex,\n} from 'drizzle-orm/sqlite-core';\nimport type { ApiError } from '../../shared/types.js';\n\nexport const operations = sqliteTable(\n 'operations',\n {\n clientOperationId: text('client_operation_id').primaryKey(),\n kind: text('kind').notNull(),\n target: text('target').notNull(),\n projectId: text('project_id'),\n requestDigest: text('request_digest').notNull(),\n state: text('state', {\n enum: ['accepted', 'dispatching', 'acknowledged', 'failed', 'unknown'],\n }).notNull(),\n result: text('result', { mode: 'json' }).$type<unknown>(),\n error: text('error', { mode: 'json' }).$type<ApiError>(),\n createdAt: integer('created_at').notNull(),\n updatedAt: integer('updated_at').notNull(),\n reconciledAt: integer('reconciled_at'),\n reconciliationDecision: text('reconciliation_decision'),\n },\n (table) => [\n check(\n 'operations_state_check',\n sql`${table.state} in ('accepted', 'dispatching', 'acknowledged', 'failed', 'unknown')`,\n ),\n index('operations_state_idx').on(table.state),\n ],\n);\n\nexport const executionMarkers = sqliteTable(\n 'execution_markers',\n {\n operationId: text('operation_id')\n .primaryKey()\n .references(() => operations.clientOperationId),\n threadId: text('thread_id').notNull(),\n turnId: text('turn_id'),\n workspaceKey: text('workspace_key').notNull(),\n codexEpoch: text('codex_epoch').notNull(),\n state: text('state', { enum: ['active', 'unknown', 'terminal', 'reconciled'] }).notNull(),\n createdAt: integer('created_at').notNull(),\n endedAt: integer('ended_at'),\n reconciledAt: integer('reconciled_at'),\n reconciliationOperationId: text('reconciliation_operation_id').references(\n () => operations.clientOperationId,\n ),\n },\n (table) => [\n check(\n 'execution_markers_state_check',\n sql`${table.state} in ('active', 'unknown', 'terminal', 'reconciled')`,\n ),\n uniqueIndex('execution_markers_active_thread')\n .on(table.threadId)\n .where(sql`${table.endedAt} is null`),\n ],\n);\n\nexport const threadLinks = sqliteTable('thread_links', {\n threadId: text('thread_id').primaryKey(),\n projectId: text('project_id').notNull(),\n cwd: text('cwd').notNull(),\n source: text('source').notNull(),\n managedEpoch: text('managed_epoch'),\n updatedAt: integer('updated_at').notNull(),\n});\n\nexport const preferences = sqliteTable('preferences', {\n key: text('key').primaryKey(),\n value: text('value', { mode: 'json' }).$type<unknown>().notNull(),\n});\n\n/** Locally observed turn totals; upstream turn history does not include token usage. */\nexport const turnUsage = sqliteTable(\n 'turn_usage',\n {\n threadId: text('thread_id').notNull(),\n turnId: text('turn_id').notNull(),\n outputTokens: integer('output_tokens').notNull(),\n },\n (table) => [primaryKey({ columns: [table.threadId, table.turnId] })],\n);\n\n/** Removing a registration retains its identity and all existing thread links. */\nexport const projectRegistrations = sqliteTable('project_registrations', {\n id: text('id').primaryKey(),\n name: text('name').notNull(),\n path: text('path').notNull(),\n canonicalRoot: text('canonical_root'),\n removedAt: integer('removed_at'),\n createdAt: integer('created_at').notNull(),\n updatedAt: integer('updated_at').notNull(),\n});\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,31 @@
1
+ import { eq } from "drizzle-orm";
2
+ import { turnUsage } from "./schema.js";
3
+ class TurnUsageStore {
4
+ constructor(database) {
5
+ this.database = database;
6
+ }
7
+ database;
8
+ save(threadId, turn) {
9
+ if (turn.outputTokens === void 0 || !Number.isSafeInteger(turn.outputTokens) || turn.outputTokens < 0)
10
+ return;
11
+ this.database.insert(turnUsage).values({
12
+ threadId,
13
+ turnId: turn.id,
14
+ outputTokens: turn.outputTokens
15
+ }).onConflictDoNothing().run();
16
+ }
17
+ restore(threadId, turns) {
18
+ if (turns.length === 0) return turns;
19
+ const saved = new Map(
20
+ this.database.select().from(turnUsage).where(eq(turnUsage.threadId, threadId)).all().map((entry) => [entry.turnId, entry.outputTokens])
21
+ );
22
+ return turns.map((turn) => {
23
+ const outputTokens = saved.get(turn.id);
24
+ return outputTokens === void 0 ? turn : { ...turn, outputTokens };
25
+ });
26
+ }
27
+ }
28
+ export {
29
+ TurnUsageStore
30
+ };
31
+ //# sourceMappingURL=turn-usage.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/server/db/turn-usage.ts"],
4
+ "sourcesContent": ["import { eq } from 'drizzle-orm';\nimport type { AppDatabase } from './index.js';\nimport { turnUsage } from './schema.js';\nimport type { TurnView } from '../../shared/types.js';\n\nexport class TurnUsageStore {\n constructor(private readonly database: AppDatabase) {}\n\n save(threadId: string, turn: TurnView): void {\n if (\n turn.outputTokens === undefined ||\n !Number.isSafeInteger(turn.outputTokens) ||\n turn.outputTokens < 0\n )\n return;\n this.database\n .insert(turnUsage)\n .values({\n threadId,\n turnId: turn.id,\n outputTokens: turn.outputTokens,\n })\n .onConflictDoNothing()\n .run();\n }\n\n restore(threadId: string, turns: TurnView[]): TurnView[] {\n if (turns.length === 0) return turns;\n const saved = new Map(\n this.database\n .select()\n .from(turnUsage)\n .where(eq(turnUsage.threadId, threadId))\n .all()\n .map((entry) => [entry.turnId, entry.outputTokens]),\n );\n return turns.map((turn) => {\n const outputTokens = saved.get(turn.id);\n return outputTokens === undefined ? turn : { ...turn, outputTokens };\n });\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,UAAU;AAEnB,SAAS,iBAAiB;AAGnB,MAAM,eAAe;AAAA,EAC1B,YAA6B,UAAuB;AAAvB;AAAA,EAAwB;AAAA,EAAxB;AAAA,EAE7B,KAAK,UAAkB,MAAsB;AAC3C,QACE,KAAK,iBAAiB,UACtB,CAAC,OAAO,cAAc,KAAK,YAAY,KACvC,KAAK,eAAe;AAEpB;AACF,SAAK,SACF,OAAO,SAAS,EAChB,OAAO;AAAA,MACN;AAAA,MACA,QAAQ,KAAK;AAAA,MACb,cAAc,KAAK;AAAA,IACrB,CAAC,EACA,oBAAoB,EACpB,IAAI;AAAA,EACT;AAAA,EAEA,QAAQ,UAAkB,OAA+B;AACvD,QAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,UAAM,QAAQ,IAAI;AAAA,MAChB,KAAK,SACF,OAAO,EACP,KAAK,SAAS,EACd,MAAM,GAAG,UAAU,UAAU,QAAQ,CAAC,EACtC,IAAI,EACJ,IAAI,CAAC,UAAU,CAAC,MAAM,QAAQ,MAAM,YAAY,CAAC;AAAA,IACtD;AACA,WAAO,MAAM,IAAI,CAAC,SAAS;AACzB,YAAM,eAAe,MAAM,IAAI,KAAK,EAAE;AACtC,aAAO,iBAAiB,SAAY,OAAO,EAAE,GAAG,MAAM,aAAa;AAAA,IACrE,CAAC;AAAA,EACH;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,123 @@
1
+ import { randomUUID } from "node:crypto";
2
+ class EventHub {
3
+ constructor(maxBytes = 16 * 1024 * 1024) {
4
+ this.maxBytes = maxBytes;
5
+ }
6
+ maxBytes;
7
+ serverEpoch = randomUUID();
8
+ seq = 0;
9
+ discardedThrough = 0;
10
+ bytes = 0;
11
+ events = [];
12
+ listeners = /* @__PURE__ */ new Set();
13
+ runtimeListeners = /* @__PURE__ */ new Set();
14
+ overviewListeners = /* @__PURE__ */ new Set();
15
+ cursor() {
16
+ return { serverEpoch: this.serverEpoch, seq: this.seq };
17
+ }
18
+ publish(kind, payload, threadId) {
19
+ const event = {
20
+ type: "event",
21
+ cursor: { serverEpoch: this.serverEpoch, seq: ++this.seq },
22
+ kind,
23
+ payload: structuredClone(payload),
24
+ ...threadId === void 0 ? {} : { threadId }
25
+ };
26
+ const bytes = Buffer.byteLength(JSON.stringify(event));
27
+ this.events.push({ event, bytes });
28
+ this.bytes += bytes;
29
+ while (this.bytes > this.maxBytes && this.events.length > 0) {
30
+ const removed = this.events.shift();
31
+ this.bytes -= removed.bytes;
32
+ this.discardedThrough = removed.event.cursor.seq;
33
+ }
34
+ for (const listener of this.listeners)
35
+ if (!event.threadId || event.threadId === listener.threadId)
36
+ this.deliver(listener.send, event);
37
+ if (kind === "runtime")
38
+ for (const listener of this.runtimeListeners) this.deliver(listener, event);
39
+ }
40
+ subscribeRuntime(send) {
41
+ this.runtimeListeners.add(send);
42
+ return () => {
43
+ this.runtimeListeners.delete(send);
44
+ };
45
+ }
46
+ /** Overview is recovered via HTTP; it never consumes the detailed replay buffer. */
47
+ publishSummary(summary) {
48
+ const event = {
49
+ type: "event",
50
+ kind: "threadSummary",
51
+ threadId: summary.id,
52
+ cursor: this.cursor(),
53
+ payload: structuredClone(summary)
54
+ };
55
+ for (const listener of this.overviewListeners) this.deliver(listener, event);
56
+ }
57
+ subscribeOverview(send) {
58
+ this.overviewListeners.add(send);
59
+ return () => {
60
+ this.overviewListeners.delete(send);
61
+ };
62
+ }
63
+ /** Only connected overview clients receive this signal. It never enters the replay buffer. */
64
+ publishCompletion(completion) {
65
+ const event = {
66
+ type: "event",
67
+ kind: "turnCompletion",
68
+ threadId: completion.threadId,
69
+ cursor: this.cursor(),
70
+ payload: structuredClone(completion)
71
+ };
72
+ for (const listener of this.overviewListeners) this.deliver(listener, event);
73
+ }
74
+ /** Project snapshots have their own update boundary even without a thread event. */
75
+ publishProjects(projects) {
76
+ const event = {
77
+ type: "event",
78
+ kind: "projects",
79
+ cursor: { serverEpoch: this.serverEpoch, seq: ++this.seq },
80
+ payload: structuredClone(projects)
81
+ };
82
+ for (const listener of this.overviewListeners) this.deliver(listener, event);
83
+ }
84
+ subscribe(threadId, snapshot, after, send) {
85
+ const replayable = after?.serverEpoch === this.serverEpoch && after.seq >= this.discardedThrough && after.seq <= this.seq;
86
+ const queued = [];
87
+ let initializing = true;
88
+ const listener = {
89
+ threadId,
90
+ send: (message) => {
91
+ if (initializing) queued.push(message);
92
+ else send(message);
93
+ }
94
+ };
95
+ this.listeners.add(listener);
96
+ let boundary = this.seq;
97
+ if (replayable && after) {
98
+ for (const { event } of [...this.events])
99
+ if (event.cursor.seq > after.seq && event.cursor.seq <= boundary && (!event.threadId || event.threadId === threadId))
100
+ this.deliver(send, event);
101
+ } else {
102
+ const data = structuredClone(snapshot());
103
+ boundary = this.seq;
104
+ this.deliver(send, { type: "snapshot", cursor: this.cursor(), threadId, data });
105
+ }
106
+ initializing = false;
107
+ for (const message of queued)
108
+ if (message.type === "event" && message.cursor.seq > boundary) this.deliver(send, message);
109
+ return () => {
110
+ this.listeners.delete(listener);
111
+ };
112
+ }
113
+ deliver(send, message) {
114
+ try {
115
+ send(message);
116
+ } catch {
117
+ }
118
+ }
119
+ }
120
+ export {
121
+ EventHub
122
+ };
123
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/server/events.ts"],
4
+ "sourcesContent": ["import { randomUUID } from 'node:crypto';\nimport type {\n Cursor,\n Operation,\n Project,\n ServerMessage,\n SystemInfo,\n ThreadSummary,\n ThreadView,\n TurnCompletion,\n} from '../shared/types.js';\n\ntype BusinessEvent = Extract<ServerMessage, { type: 'event' }>;\ntype Listener = (message: ServerMessage) => void;\n\n/** Synchronous snapshot+subscription forms one JS event-loop boundary. */\nexport class EventHub {\n readonly serverEpoch = randomUUID();\n private seq = 0;\n private discardedThrough = 0;\n private bytes = 0;\n private readonly events: { event: BusinessEvent; bytes: number }[] = [];\n private readonly listeners = new Set<{ threadId: string; send: Listener }>();\n private readonly runtimeListeners = new Set<Listener>();\n private readonly overviewListeners = new Set<Listener>();\n\n constructor(private readonly maxBytes = 16 * 1024 * 1024) {}\n\n cursor(): Cursor {\n return { serverEpoch: this.serverEpoch, seq: this.seq };\n }\n\n publish(\n kind: 'thread' | 'runtime' | 'operation',\n payload: ThreadView | SystemInfo | Operation,\n threadId?: string,\n ): void {\n const event: BusinessEvent = {\n type: 'event',\n cursor: { serverEpoch: this.serverEpoch, seq: ++this.seq },\n kind,\n payload: structuredClone(payload),\n ...(threadId === undefined ? {} : { threadId }),\n };\n const bytes = Buffer.byteLength(JSON.stringify(event));\n this.events.push({ event, bytes });\n this.bytes += bytes;\n while (this.bytes > this.maxBytes && this.events.length > 0) {\n const removed = this.events.shift()!;\n this.bytes -= removed.bytes;\n this.discardedThrough = removed.event.cursor.seq;\n }\n for (const listener of this.listeners)\n if (!event.threadId || event.threadId === listener.threadId)\n this.deliver(listener.send, event);\n if (kind === 'runtime')\n for (const listener of this.runtimeListeners) this.deliver(listener, event);\n }\n\n subscribeRuntime(send: Listener): () => void {\n this.runtimeListeners.add(send);\n return () => {\n this.runtimeListeners.delete(send);\n };\n }\n\n /** Overview is recovered via HTTP; it never consumes the detailed replay buffer. */\n publishSummary(summary: ThreadSummary): void {\n const event: BusinessEvent = {\n type: 'event',\n kind: 'threadSummary',\n threadId: summary.id,\n cursor: this.cursor(),\n payload: structuredClone(summary),\n };\n for (const listener of this.overviewListeners) this.deliver(listener, event);\n }\n\n subscribeOverview(send: Listener): () => void {\n this.overviewListeners.add(send);\n return () => {\n this.overviewListeners.delete(send);\n };\n }\n\n /** Only connected overview clients receive this signal. It never enters the replay buffer. */\n publishCompletion(completion: TurnCompletion): void {\n const event: BusinessEvent = {\n type: 'event',\n kind: 'turnCompletion',\n threadId: completion.threadId,\n cursor: this.cursor(),\n payload: structuredClone(completion),\n };\n for (const listener of this.overviewListeners) this.deliver(listener, event);\n }\n\n /** Project snapshots have their own update boundary even without a thread event. */\n publishProjects(projects: Project[]): void {\n const event: BusinessEvent = {\n type: 'event',\n kind: 'projects',\n cursor: { serverEpoch: this.serverEpoch, seq: ++this.seq },\n payload: structuredClone(projects),\n };\n for (const listener of this.overviewListeners) this.deliver(listener, event);\n }\n\n subscribe(\n threadId: string,\n snapshot: () => ThreadView,\n after: Cursor | undefined,\n send: Listener,\n ): () => void {\n const replayable =\n after?.serverEpoch === this.serverEpoch &&\n after.seq >= this.discardedThrough &&\n after.seq <= this.seq;\n const queued: ServerMessage[] = [];\n let initializing = true;\n const listener = {\n threadId,\n send: (message: ServerMessage) => {\n if (initializing) queued.push(message);\n else send(message);\n },\n };\n this.listeners.add(listener);\n // No await may occur between reading the snapshot and registering the listener.\n let boundary = this.seq;\n if (replayable && after) {\n for (const { event } of [...this.events])\n if (\n event.cursor.seq > after.seq &&\n event.cursor.seq <= boundary &&\n (!event.threadId || event.threadId === threadId)\n )\n this.deliver(send, event);\n } else {\n const data = structuredClone(snapshot());\n boundary = this.seq;\n this.deliver(send, { type: 'snapshot', cursor: this.cursor(), threadId, data });\n }\n initializing = false;\n for (const message of queued)\n if (message.type === 'event' && message.cursor.seq > boundary) this.deliver(send, message);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n private deliver(send: Listener, message: ServerMessage): void {\n // A closed or slow browser must never unwind the upstream event consumer.\n try {\n send(message);\n } catch {\n /* Transport closes or unsubscribes its own failed socket. */\n }\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,305 @@
1
+ import { Hono } from "hono";
2
+ import { bodyLimit } from "hono/body-limit";
3
+ import { serveStatic } from "@hono/node-server/serve-static";
4
+ import { upgradeWebSocket } from "@hono/node-server";
5
+ import { ZodError } from "zod";
6
+ import { AppError } from "../shared/errors.js";
7
+ import { IMAGE_REQUEST_BYTES } from "../shared/images.js";
8
+ import * as schemas from "../shared/validation.js";
9
+ import { ConfigStore } from "./config-store.js";
10
+ import { createAuthenticator } from "./auth.js";
11
+ import { socketHandlers } from "./websocket.js";
12
+ import { z } from "zod";
13
+ import { releaseChannels } from "../shared/updates.js";
14
+ import { settingsSchema, updateRequestSchema } from "../updates/manager.js";
15
+ function createApp({
16
+ config,
17
+ service,
18
+ staticDir,
19
+ authenticate = createAuthenticator(config),
20
+ updates
21
+ }) {
22
+ const app = new Hono();
23
+ app.onError((error, c) => {
24
+ const failure = error instanceof AppError ? error : error instanceof ZodError || error instanceof SyntaxError ? new AppError("VALIDATION_FAILED", "Invalid request data", 400) : new AppError("INTERNAL_ERROR", "An internal error occurred", 500);
25
+ return c.json({ error: failure.toJSON(c.get("operationId")) }, failure.status);
26
+ });
27
+ app.get("/api/health", (c) => c.json({ ok: true }));
28
+ app.use("*", async (c, next) => {
29
+ c.set("identity", await authenticate(c.req.raw, c.req.path === "/ws"));
30
+ await next();
31
+ });
32
+ app.use(
33
+ "/api/*",
34
+ (c, next) => bodyLimit({
35
+ maxSize: /^\/api\/threads\/[^/]+\/(turns|steer)$/.test(c.req.path) ? IMAGE_REQUEST_BYTES : 512 * 1024,
36
+ onError: () => {
37
+ throw new AppError("BODY_TOO_LARGE", "Request body exceeds limit", 413);
38
+ }
39
+ })(c, next)
40
+ );
41
+ app.use("/api/*", async (c, next) => {
42
+ c.header("Cache-Control", "no-store");
43
+ if (service.isInMaintenance() && !["GET", "HEAD", "OPTIONS"].includes(c.req.method) && !c.req.path.startsWith("/api/updates"))
44
+ throw new AppError("UPDATE_MAINTENANCE", "\u6B63\u5728\u5347\u7EA7\uFF0C\u8BF7\u7B49\u5F85\u670D\u52A1\u91CD\u65B0\u8FDE\u63A5", 503);
45
+ await next();
46
+ });
47
+ if (updates) {
48
+ app.get("/api/updates", async (c) => c.json(await updates.status()));
49
+ app.post("/api/updates/check", async (c) => {
50
+ const input = z.object({ channel: z.enum(releaseChannels).optional() }).strict().parse(await c.req.json());
51
+ return c.json(await updates.check(input.channel));
52
+ });
53
+ app.post(
54
+ "/api/updates/settings",
55
+ async (c) => c.json(await updates.configure(settingsSchema.parse(await c.req.json())))
56
+ );
57
+ app.post(
58
+ "/api/updates/run",
59
+ async (c) => c.json(await updates.start(updateRequestSchema.parse(await c.req.json())), 202)
60
+ );
61
+ }
62
+ const id = (value) => schemas.idSchema.parse(value);
63
+ const parse = async (c, schema) => {
64
+ const value = await c.req.json();
65
+ if (value && typeof value === "object" && "clientOperationId" in value && typeof value.clientOperationId === "string")
66
+ c.set("operationId", value.clientOperationId);
67
+ return schema.parse(value);
68
+ };
69
+ const status = (op) => op.status === "dispatching" || op.status === "accepted" ? 202 : 200;
70
+ const configuration = new ConfigStore(config);
71
+ app.use("/api/config*", async (c, next) => {
72
+ c.header("Cache-Control", "no-store");
73
+ await next();
74
+ });
75
+ app.get("/api/config", async (c) => c.json(await configuration.snapshot()));
76
+ app.post("/api/config", async (c) => c.json(await configuration.save(await c.req.json())));
77
+ app.post(
78
+ "/api/config/import",
79
+ async (c) => c.json(await configuration.previewImport(await c.req.json()))
80
+ );
81
+ app.get("/api/config/export", async (c) => {
82
+ c.header("Content-Disposition", 'attachment; filename="codex-web-config.json"');
83
+ return c.json(await configuration.export());
84
+ });
85
+ app.get("/api/system", (c) => c.json(service.getSystem()));
86
+ app.post("/api/system/compatibility/confirm", async (c) => {
87
+ const op = await service.confirmCompatibility(
88
+ await parse(c, schemas.confirmCompatibilitySchema)
89
+ );
90
+ return c.json(op, status(op));
91
+ });
92
+ app.get("/api/projects", async (c) => c.json({ data: await service.listProjects() }));
93
+ app.get(
94
+ "/api/projects/:projectId/runtime-settings",
95
+ async (c) => c.json(await service.readRuntimeSettings(id(c.req.param("projectId"))))
96
+ );
97
+ app.post("/api/projects/:projectId/model-defaults", async (c) => {
98
+ const op = await service.saveModelDefaults(
99
+ id(c.req.param("projectId")),
100
+ await parse(c, schemas.saveModelDefaultsSchema)
101
+ );
102
+ return c.json(op, status(op));
103
+ });
104
+ app.post(
105
+ "/api/projects",
106
+ async (c) => c.json(service.addProject(await parse(c, schemas.createProjectSchema)))
107
+ );
108
+ app.post(
109
+ "/api/projects/:projectId/rename",
110
+ async (c) => c.json(
111
+ service.renameProject(
112
+ id(c.req.param("projectId")),
113
+ await parse(c, schemas.renameProjectSchema)
114
+ )
115
+ )
116
+ );
117
+ app.post("/api/projects/:projectId/remove", async (c) => {
118
+ await parse(c, schemas.removeProjectSchema);
119
+ return c.json(service.removeProject(id(c.req.param("projectId"))));
120
+ });
121
+ app.get(
122
+ "/api/projects/:projectId/threads",
123
+ async (c) => c.json(
124
+ await service.listThreadSummaries(
125
+ id(c.req.param("projectId")),
126
+ c.req.query("cursor") === void 0 ? void 0 : schemas.cursorSchema.parse(c.req.query("cursor")),
127
+ schemas.archivedQuerySchema.parse(c.req.query("archived"))
128
+ )
129
+ )
130
+ );
131
+ app.get(
132
+ "/api/threads",
133
+ async (c) => c.json(
134
+ await service.listThreads(
135
+ id(c.req.query("projectId") ?? ""),
136
+ c.req.query("cursor") === void 0 ? void 0 : schemas.cursorSchema.parse(c.req.query("cursor")),
137
+ schemas.archivedQuerySchema.parse(c.req.query("archived"))
138
+ )
139
+ )
140
+ );
141
+ app.post("/api/threads/:threadId/archive", async (c) => {
142
+ return c.json(
143
+ await service.setThreadArchived(
144
+ id(c.req.param("threadId")),
145
+ await parse(c, schemas.archiveThreadSchema)
146
+ )
147
+ );
148
+ });
149
+ app.get(
150
+ "/api/threads/:threadId",
151
+ async (c) => c.json(await service.getThread(id(c.req.param("threadId"))))
152
+ );
153
+ app.get(
154
+ "/api/threads/:threadId/metadata",
155
+ async (c) => c.json(await service.getThreadMetadata(id(c.req.param("threadId"))))
156
+ );
157
+ app.get("/api/threads/:threadId/export", async (c) => {
158
+ const exported = await service.exportThread(id(c.req.param("threadId")));
159
+ c.header("Content-Type", "text/markdown; charset=utf-8");
160
+ c.header("Content-Disposition", `attachment; filename="${exported.filename}"`);
161
+ c.header("Cache-Control", "no-store");
162
+ c.header("X-Content-Type-Options", "nosniff");
163
+ c.header("X-Export-Completeness", exported.partial ? "partial" : "saved-history");
164
+ return c.body(exported.markdown);
165
+ });
166
+ app.get("/api/threads/:threadId/images", async (c) => {
167
+ c.header("Cache-Control", "private, no-store");
168
+ c.header("X-Content-Type-Options", "nosniff");
169
+ c.header("Cross-Origin-Resource-Policy", "same-origin");
170
+ c.header("Content-Security-Policy", "default-src 'none'; sandbox");
171
+ const image = await service.getThreadImage(id(c.req.param("threadId")), c.req.query("path"));
172
+ return c.body(image.bytes, 200, {
173
+ "Content-Type": image.contentType,
174
+ "Content-Length": String(image.bytes.length)
175
+ });
176
+ });
177
+ app.get("/api/threads/:threadId/markdown", async (c) => {
178
+ c.header("Cache-Control", "private, no-store");
179
+ c.header("X-Content-Type-Options", "nosniff");
180
+ c.header("Cross-Origin-Resource-Policy", "same-origin");
181
+ return c.json(
182
+ await service.getThreadMarkdown(id(c.req.param("threadId")), c.req.query("path"))
183
+ );
184
+ });
185
+ app.get(
186
+ "/api/operations/:operationId",
187
+ (c) => c.json(service.getOperation(id(c.req.param("operationId"))))
188
+ );
189
+ app.post("/api/projects/:projectId/threads", async (c) => {
190
+ const op = await service.createThread(
191
+ id(c.req.param("projectId")),
192
+ await parse(c, schemas.createThreadSchema)
193
+ );
194
+ return c.json(op, status(op));
195
+ });
196
+ app.post("/api/threads/:threadId/resume", async (c) => {
197
+ const op = await service.resumeThread(
198
+ id(c.req.param("threadId")),
199
+ await parse(c, schemas.resumeThreadSchema)
200
+ );
201
+ return c.json(op, status(op));
202
+ });
203
+ app.post("/api/threads/:threadId/fork", async (c) => {
204
+ const op = await service.forkThread(
205
+ id(c.req.param("threadId")),
206
+ await parse(c, schemas.forkThreadSchema)
207
+ );
208
+ return c.json(op, status(op));
209
+ });
210
+ app.post("/api/threads/:threadId/rename", async (c) => {
211
+ const op = await service.renameThread(
212
+ id(c.req.param("threadId")),
213
+ await parse(c, schemas.renameThreadSchema)
214
+ );
215
+ return c.json(op, status(op));
216
+ });
217
+ app.post("/api/threads/:threadId/turns", async (c) => {
218
+ const op = await service.startTurn(
219
+ id(c.req.param("threadId")),
220
+ await parse(c, schemas.startTurnSchema)
221
+ );
222
+ return c.json(op, status(op));
223
+ });
224
+ app.post("/api/threads/:threadId/review", async (c) => {
225
+ const op = await service.startReview(
226
+ id(c.req.param("threadId")),
227
+ await parse(c, schemas.startReviewSchema)
228
+ );
229
+ return c.json(op, status(op));
230
+ });
231
+ app.post("/api/threads/:threadId/steer", async (c) => {
232
+ const op = await service.steer(
233
+ id(c.req.param("threadId")),
234
+ await parse(c, schemas.steerSchema)
235
+ );
236
+ return c.json(op, status(op));
237
+ });
238
+ app.post("/api/threads/:threadId/compact", async (c) => {
239
+ const op = await service.compactThread(
240
+ id(c.req.param("threadId")),
241
+ await parse(c, schemas.operationSchema)
242
+ );
243
+ return c.json(op, status(op));
244
+ });
245
+ app.post("/api/threads/:threadId/interrupt", async (c) => {
246
+ const op = await service.interrupt(
247
+ id(c.req.param("threadId")),
248
+ await parse(c, schemas.interruptSchema)
249
+ );
250
+ return c.json(op, status(op));
251
+ });
252
+ app.post("/api/actions/:actionId/resolve", async (c) => {
253
+ const op = await service.resolveAction(
254
+ id(c.req.param("actionId")),
255
+ await parse(c, schemas.resolveActionSchema)
256
+ );
257
+ return c.json(op, status(op));
258
+ });
259
+ app.post("/api/operations/:operationId/reconcile", async (c) => {
260
+ const op = await service.reconcileOperation(
261
+ id(c.req.param("operationId")),
262
+ await parse(c, schemas.reconcileSchema)
263
+ );
264
+ return c.json(op, status(op));
265
+ });
266
+ app.get(
267
+ "/ws",
268
+ upgradeWebSocket(
269
+ (c) => socketHandlers(service, config, c.get("identity"), c.req.query("overview") === "1")
270
+ )
271
+ );
272
+ app.all("/api/*", () => {
273
+ throw new AppError("NOT_FOUND", "API route not found", 404);
274
+ });
275
+ app.all("/api", () => {
276
+ throw new AppError("NOT_FOUND", "API route not found", 404);
277
+ });
278
+ app.all("/ws/*", () => {
279
+ throw new AppError("NOT_FOUND", "WebSocket route not found", 404);
280
+ });
281
+ app.use("*", async (c, next) => {
282
+ if (!c.req.path.startsWith("/assets/")) c.header("Cache-Control", "no-store");
283
+ c.header("X-Content-Type-Options", "nosniff");
284
+ c.header("Referrer-Policy", "same-origin");
285
+ c.header(
286
+ "Content-Security-Policy",
287
+ // Remote Markdown images load directly in the browser, including HTTPS redirects.
288
+ // Keep scripts, connections and all other resource policies restricted as before.
289
+ "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'"
290
+ );
291
+ await next();
292
+ });
293
+ app.get("*", serveStatic({ root: staticDir }));
294
+ app.get("*", async (c, next) => {
295
+ if (!c.req.header("accept")?.includes("text/html") || /\.[^/]+$/.test(c.req.path))
296
+ return c.json({ error: { code: "NOT_FOUND", message: "Resource not found" } }, 404);
297
+ return serveStatic({ path: `${staticDir}/index.html` })(c, next);
298
+ });
299
+ app.notFound((c) => c.json({ error: { code: "NOT_FOUND", message: "Resource not found" } }, 404));
300
+ return app;
301
+ }
302
+ export {
303
+ createApp
304
+ };
305
+ //# sourceMappingURL=http.js.map