@cospacehq/server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-typecheck.log +4 -0
  3. package/dist/agent-runtime.d.ts +45 -0
  4. package/dist/agent-runtime.d.ts.map +1 -0
  5. package/dist/agent-runtime.js +374 -0
  6. package/dist/agent-runtime.js.map +1 -0
  7. package/dist/config.d.ts +8 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +14 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/db.d.ts +128 -0
  12. package/dist/db.d.ts.map +1 -0
  13. package/dist/db.js +854 -0
  14. package/dist/db.js.map +1 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +34 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/model-client.d.ts +43 -0
  20. package/dist/model-client.d.ts.map +1 -0
  21. package/dist/model-client.js +138 -0
  22. package/dist/model-client.js.map +1 -0
  23. package/dist/provider-crypto.d.ts +4 -0
  24. package/dist/provider-crypto.d.ts.map +1 -0
  25. package/dist/provider-crypto.js +30 -0
  26. package/dist/provider-crypto.js.map +1 -0
  27. package/dist/sandbox.d.ts +3 -0
  28. package/dist/sandbox.d.ts.map +1 -0
  29. package/dist/sandbox.js +22 -0
  30. package/dist/sandbox.js.map +1 -0
  31. package/dist/server.d.ts +15 -0
  32. package/dist/server.d.ts.map +1 -0
  33. package/dist/server.js +1296 -0
  34. package/dist/server.js.map +1 -0
  35. package/package.json +33 -0
  36. package/src/agent-runtime.ts +479 -0
  37. package/src/config.ts +21 -0
  38. package/src/db.ts +1197 -0
  39. package/src/index.ts +44 -0
  40. package/src/model-client.ts +187 -0
  41. package/src/provider-crypto.ts +39 -0
  42. package/src/sandbox.ts +26 -0
  43. package/src/server.ts +1548 -0
  44. package/tsconfig.json +8 -0
package/src/index.ts ADDED
@@ -0,0 +1,44 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import { CoSpaceConfigSchema } from "@cospacehq/shared";
5
+ import { startCoSpaceServer } from "./server.js";
6
+
7
+ export { startCoSpaceServer } from "./server.js";
8
+
9
+ async function runStandaloneServer(): Promise<void> {
10
+ const configPathArg = process.argv.find((value) => value.startsWith("--config="));
11
+ const configPath = configPathArg
12
+ ? path.resolve(configPathArg.replace("--config=", ""))
13
+ : path.resolve(process.cwd(), ".cospace/config.json");
14
+
15
+ const raw = await fs.readFile(configPath, "utf8");
16
+ const config = CoSpaceConfigSchema.parse(JSON.parse(raw));
17
+
18
+ const started = await startCoSpaceServer({
19
+ config,
20
+ configPath,
21
+ webDistPath: process.env.COSPACE_WEB_DIST
22
+ });
23
+
24
+ process.stdout.write(`CoSpace server listening at ${started.localUrl}\n`);
25
+
26
+ const shutdown = async (): Promise<void> => {
27
+ await started.stop();
28
+ process.exit(0);
29
+ };
30
+
31
+ process.on("SIGINT", shutdown);
32
+ process.on("SIGTERM", shutdown);
33
+ }
34
+
35
+ const isEntry = process.argv[1] ? import.meta.url.endsWith(process.argv[1]) : false;
36
+
37
+ if (isEntry) {
38
+ runStandaloneServer().catch((error: unknown) => {
39
+ process.stderr.write(
40
+ `Failed to start CoSpace server: ${error instanceof Error ? error.message : String(error)}\n`
41
+ );
42
+ process.exit(1);
43
+ });
44
+ }
@@ -0,0 +1,187 @@
1
+ import type { ProviderKind } from "@cospacehq/shared";
2
+
3
+ export type ProviderRuntimeConfig = {
4
+ providerKey: string;
5
+ label: string;
6
+ kind: ProviderKind;
7
+ baseUrl: string;
8
+ apiKey: string;
9
+ };
10
+
11
+ export type ModelReply = {
12
+ body: string;
13
+ trace: string;
14
+ };
15
+
16
+ function normalizeBaseUrl(value: string): string {
17
+ return value.endsWith("/") ? value : `${value}/`;
18
+ }
19
+
20
+ function extractMessageContent(payload: unknown): string {
21
+ if (!payload || typeof payload !== "object") {
22
+ throw new Error("Model response payload is empty");
23
+ }
24
+
25
+ const choices = (payload as { choices?: unknown }).choices;
26
+ if (!Array.isArray(choices) || choices.length === 0) {
27
+ throw new Error("Model response has no choices");
28
+ }
29
+
30
+ const firstChoice = choices[0] as { message?: { content?: unknown } };
31
+ const content = firstChoice.message?.content;
32
+
33
+ if (typeof content === "string") {
34
+ return content;
35
+ }
36
+
37
+ if (Array.isArray(content)) {
38
+ const textFragments = content
39
+ .map((entry) => {
40
+ if (!entry || typeof entry !== "object") {
41
+ return "";
42
+ }
43
+
44
+ const typed = entry as { type?: string; text?: string };
45
+ return typed.type === "text" && typeof typed.text === "string" ? typed.text : "";
46
+ })
47
+ .filter(Boolean);
48
+
49
+ if (textFragments.length > 0) {
50
+ return textFragments.join("\n");
51
+ }
52
+ }
53
+
54
+ throw new Error("Model response content is not text");
55
+ }
56
+
57
+ function parseStructuredReply(text: string, includeTrace: boolean): ModelReply {
58
+ const candidate = text.trim();
59
+
60
+ try {
61
+ const parsed = JSON.parse(candidate) as { body?: unknown; trace?: unknown };
62
+ if (typeof parsed.body === "string" && parsed.body.trim().length > 0) {
63
+ return {
64
+ body: parsed.body.trim(),
65
+ trace: includeTrace
66
+ ? typeof parsed.trace === "string" && parsed.trace.trim().length > 0
67
+ ? parsed.trace.trim()
68
+ : "No trace available"
69
+ : ""
70
+ };
71
+ }
72
+ } catch {
73
+ // Non-JSON response is handled below.
74
+ }
75
+
76
+ return {
77
+ body: candidate,
78
+ trace: includeTrace ? "Model returned an unstructured response" : ""
79
+ };
80
+ }
81
+
82
+ export class OpenAICompatibleModelClient {
83
+ public async complete(input: {
84
+ provider: ProviderRuntimeConfig;
85
+ model: string;
86
+ messages: Array<{ role: "system" | "user" | "assistant"; content: string }>;
87
+ temperature?: number;
88
+ }): Promise<string> {
89
+ if (input.provider.kind !== "openai-compatible") {
90
+ throw new Error(`Unsupported provider kind: ${input.provider.kind}`);
91
+ }
92
+
93
+ const endpoint = new URL("chat/completions", normalizeBaseUrl(input.provider.baseUrl)).toString();
94
+ const controller = new AbortController();
95
+ const timeout = setTimeout(() => controller.abort(), 30_000);
96
+
97
+ try {
98
+ const response = await fetch(endpoint, {
99
+ method: "POST",
100
+ signal: controller.signal,
101
+ headers: {
102
+ "content-type": "application/json",
103
+ authorization: `Bearer ${input.provider.apiKey}`
104
+ },
105
+ body: JSON.stringify({
106
+ model: input.model,
107
+ temperature: input.temperature ?? 0.2,
108
+ messages: input.messages
109
+ })
110
+ });
111
+
112
+ if (!response.ok) {
113
+ const details = await response.text();
114
+ throw new Error(`Provider request failed (${response.status}): ${details.slice(0, 240)}`);
115
+ }
116
+
117
+ const payload = (await response.json()) as unknown;
118
+ return extractMessageContent(payload);
119
+ } finally {
120
+ clearTimeout(timeout);
121
+ }
122
+ }
123
+
124
+ public async testConnection(input: {
125
+ provider: ProviderRuntimeConfig;
126
+ model: string;
127
+ }): Promise<{ preview: string }> {
128
+ const text = await this.complete({
129
+ provider: input.provider,
130
+ model: input.model,
131
+ temperature: 0,
132
+ messages: [
133
+ {
134
+ role: "system",
135
+ content: "You are testing model connectivity. Reply with one short sentence that includes the token CONNECTED."
136
+ },
137
+ {
138
+ role: "user",
139
+ content: "Respond now."
140
+ }
141
+ ]
142
+ });
143
+
144
+ return { preview: text.slice(0, 180) };
145
+ }
146
+
147
+ public async generateAgentReply(input: {
148
+ provider: ProviderRuntimeConfig;
149
+ model: string;
150
+ agentTitle: string;
151
+ agentName: string;
152
+ agentInstruction: string | null;
153
+ latestMessages: Array<{ senderName: string; senderRole: "human" | "agent"; body: string }>;
154
+ includeTrace: boolean;
155
+ }): Promise<ModelReply> {
156
+ const conversation = input.latestMessages
157
+ .map((message) => `${message.senderRole === "human" ? "Human" : "Agent"} ${message.senderName}: ${message.body}`)
158
+ .join("\n");
159
+
160
+ const text = await this.complete({
161
+ provider: input.provider,
162
+ model: input.model,
163
+ messages: [
164
+ {
165
+ role: "system",
166
+ content: [
167
+ `You are ${input.agentName}, the ${input.agentTitle} in CoSpace.`,
168
+ input.agentInstruction ? `Role instruction: ${input.agentInstruction}` : "",
169
+ "Keep the response concise and practical.",
170
+ input.includeTrace ? "Return strict JSON with keys body and trace." : "Return strict JSON with key body.",
171
+ input.includeTrace
172
+ ? "trace should be a short 2-4 line execution summary, not hidden chain-of-thought."
173
+ : ""
174
+ ]
175
+ .filter(Boolean)
176
+ .join(" ")
177
+ },
178
+ {
179
+ role: "user",
180
+ content: `Conversation context:\n${conversation}\n\nWrite your next response.`
181
+ }
182
+ ]
183
+ });
184
+
185
+ return parseStructuredReply(text, input.includeTrace);
186
+ }
187
+ }
@@ -0,0 +1,39 @@
1
+ import crypto from "node:crypto";
2
+
3
+ const ENCRYPTION_VERSION = "v1";
4
+
5
+ function deriveKey(secret: string): Buffer {
6
+ return crypto.scryptSync(secret, "cospace-provider-credentials", 32);
7
+ }
8
+
9
+ export function encryptionSecretFromToken(token: string): string {
10
+ return process.env.COSPACE_ENCRYPTION_SECRET?.trim() || token;
11
+ }
12
+
13
+ export function encryptProviderSecret(secretValue: string, secret: string): string {
14
+ const key = deriveKey(secret);
15
+ const iv = crypto.randomBytes(12);
16
+ const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
17
+
18
+ const encrypted = Buffer.concat([cipher.update(secretValue, "utf8"), cipher.final()]);
19
+ const authTag = cipher.getAuthTag();
20
+
21
+ return `${ENCRYPTION_VERSION}:${iv.toString("base64")}:${authTag.toString("base64")}:${encrypted.toString("base64")}`;
22
+ }
23
+
24
+ export function decryptProviderSecret(encoded: string, secret: string): string {
25
+ const [version, ivRaw, tagRaw, payloadRaw] = encoded.split(":");
26
+ if (version !== ENCRYPTION_VERSION || !ivRaw || !tagRaw || !payloadRaw) {
27
+ throw new Error("Unsupported provider secret format");
28
+ }
29
+
30
+ const key = deriveKey(secret);
31
+ const iv = Buffer.from(ivRaw, "base64");
32
+ const authTag = Buffer.from(tagRaw, "base64");
33
+ const payload = Buffer.from(payloadRaw, "base64");
34
+
35
+ const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
36
+ decipher.setAuthTag(authTag);
37
+
38
+ return Buffer.concat([decipher.update(payload), decipher.final()]).toString("utf8");
39
+ }
package/src/sandbox.ts ADDED
@@ -0,0 +1,26 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs/promises";
3
+
4
+ export function resolveSandboxPath(root: string, requestedPath: string): string {
5
+ const normalizedRoot = path.resolve(root);
6
+ const target = path.resolve(normalizedRoot, requestedPath);
7
+
8
+ if (target !== normalizedRoot && !target.startsWith(`${normalizedRoot}${path.sep}`)) {
9
+ throw new Error("Path escapes workspace sandbox");
10
+ }
11
+
12
+ return target;
13
+ }
14
+
15
+ export async function listSandboxFiles(root: string, requestedPath: string): Promise<string[]> {
16
+ const target = resolveSandboxPath(root, requestedPath);
17
+ const entries = await fs.readdir(target, { withFileTypes: true });
18
+
19
+ return entries
20
+ .map((entry) => ({
21
+ entry,
22
+ relativePath: path.relative(root, path.join(target, entry.name))
23
+ }))
24
+ .sort((a, b) => a.relativePath.localeCompare(b.relativePath))
25
+ .map(({ entry, relativePath }) => (entry.isDirectory() ? `${relativePath}/` : relativePath));
26
+ }