Add AgentOS daemon control plane and maintenance tooling
This commit is contained in:
@@ -7,7 +7,8 @@
|
||||
"build": "flue build",
|
||||
"check-types": "tsc --noEmit",
|
||||
"dev": "flue dev",
|
||||
"run": "flue run"
|
||||
"run": "flue run",
|
||||
"run:zopu": "flue run zopu"
|
||||
},
|
||||
"dependencies": {
|
||||
"@flue/runtime": "latest"
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { defineAgent } from '@flue/runtime';
|
||||
|
||||
export default defineAgent(() => ({
|
||||
model: 'anthropic/claude-sonnet-4-6',
|
||||
instructions: 'Give a concise, helpful hello-world response for this project.',
|
||||
}));
|
||||
11
packages/agents/src/agents/zopu.ts
Normal file
11
packages/agents/src/agents/zopu.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineAgent } from "@flue/runtime";
|
||||
|
||||
export default defineAgent(() => ({
|
||||
description: "Zopu is the primary coding agent users collaborate with.",
|
||||
model: "anthropic/claude-sonnet-4-6",
|
||||
instructions: `You are Zopu, the primary agent users collaborate with.
|
||||
|
||||
Work as a pragmatic senior coding agent. Understand the user's goal, inspect the current workspace, make the necessary changes, and verify that the result works. Prefer direct, maintainable solutions over unnecessary abstraction.
|
||||
|
||||
Your workspace is an isolated agentOS VM dedicated to this agent. Treat its filesystem, processes, and session state as the complete working environment. Use the capabilities available in the workspace, and do not assume access to resources outside it.`,
|
||||
}));
|
||||
6
packages/backend/convex/_generated/api.d.ts
vendored
6
packages/backend/convex/_generated/api.d.ts
vendored
@@ -9,6 +9,9 @@
|
||||
*/
|
||||
|
||||
import type * as auth from "../auth.js";
|
||||
import type * as daemonCommands from "../daemonCommands.js";
|
||||
import type * as daemonRuntime from "../daemonRuntime.js";
|
||||
import type * as daemons from "../daemons.js";
|
||||
import type * as healthCheck from "../healthCheck.js";
|
||||
import type * as http from "../http.js";
|
||||
import type * as privateData from "../privateData.js";
|
||||
@@ -22,6 +25,9 @@ import type {
|
||||
|
||||
declare const fullApi: ApiFromModules<{
|
||||
auth: typeof auth;
|
||||
daemonCommands: typeof daemonCommands;
|
||||
daemonRuntime: typeof daemonRuntime;
|
||||
daemons: typeof daemons;
|
||||
healthCheck: typeof healthCheck;
|
||||
http: typeof http;
|
||||
privateData: typeof privateData;
|
||||
|
||||
199
packages/backend/convex/daemonCommands.ts
Normal file
199
packages/backend/convex/daemonCommands.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import { v } from "convex/values";
|
||||
|
||||
import { mutation, query } from "./_generated/server";
|
||||
|
||||
const commandStatus = v.union(
|
||||
v.literal("queued"),
|
||||
v.literal("claimed"),
|
||||
v.literal("running"),
|
||||
v.literal("succeeded"),
|
||||
v.literal("failed"),
|
||||
v.literal("cancelled")
|
||||
);
|
||||
|
||||
export const enqueue = mutation({
|
||||
args: {
|
||||
daemonId: v.string(),
|
||||
method: v.string(),
|
||||
args: v.any(),
|
||||
actorKey: v.array(v.string()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const timestamp = Date.now();
|
||||
return await ctx.db.insert("daemonCommands", {
|
||||
...args,
|
||||
status: "queued",
|
||||
attempts: 0,
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const list = query({
|
||||
args: {
|
||||
daemonId: v.string(),
|
||||
status: v.optional(commandStatus),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
if (args.status) {
|
||||
return await ctx.db
|
||||
.query("daemonCommands")
|
||||
.withIndex("by_daemonId_and_status", (q) =>
|
||||
q.eq("daemonId", args.daemonId).eq("status", args.status!)
|
||||
)
|
||||
.order("desc")
|
||||
.take(100);
|
||||
}
|
||||
return await ctx.db
|
||||
.query("daemonCommands")
|
||||
.filter((q) => q.eq(q.field("daemonId"), args.daemonId))
|
||||
.order("desc")
|
||||
.take(100);
|
||||
},
|
||||
});
|
||||
|
||||
export const available = query({
|
||||
args: { daemonId: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
const queued = await ctx.db
|
||||
.query("daemonCommands")
|
||||
.withIndex("by_daemonId_and_status", (q) =>
|
||||
q.eq("daemonId", args.daemonId).eq("status", "queued")
|
||||
)
|
||||
.order("asc")
|
||||
.take(25);
|
||||
const expiredClaims = await ctx.db
|
||||
.query("daemonCommands")
|
||||
.withIndex("by_daemonId_and_status", (q) =>
|
||||
q.eq("daemonId", args.daemonId).eq("status", "claimed")
|
||||
)
|
||||
.filter((q) => q.lt(q.field("leaseExpiresAt"), Date.now()))
|
||||
.order("asc")
|
||||
.take(25);
|
||||
return [...expiredClaims, ...queued].slice(0, 25);
|
||||
},
|
||||
});
|
||||
|
||||
export const claim = mutation({
|
||||
args: {
|
||||
commandId: v.id("daemonCommands"),
|
||||
daemonId: v.string(),
|
||||
sessionId: v.string(),
|
||||
leaseDurationMs: v.number(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const command = await ctx.db.get("daemonCommands", args.commandId);
|
||||
if (!command || command.daemonId !== args.daemonId) {
|
||||
return null;
|
||||
}
|
||||
const timestamp = Date.now();
|
||||
const canClaim =
|
||||
command.status === "queued" ||
|
||||
(command.status === "claimed" &&
|
||||
command.leaseExpiresAt !== undefined &&
|
||||
command.leaseExpiresAt <= timestamp);
|
||||
if (!canClaim) {
|
||||
return null;
|
||||
}
|
||||
await ctx.db.patch("daemonCommands", command._id, {
|
||||
status: "claimed",
|
||||
claimedBySessionId: args.sessionId,
|
||||
leaseExpiresAt: timestamp + args.leaseDurationMs,
|
||||
attempts: command.attempts + 1,
|
||||
updatedAt: timestamp,
|
||||
});
|
||||
return { ...command, status: "claimed" as const };
|
||||
},
|
||||
});
|
||||
|
||||
export const start = mutation({
|
||||
args: {
|
||||
commandId: v.id("daemonCommands"),
|
||||
sessionId: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const command = await ctx.db.get("daemonCommands", args.commandId);
|
||||
if (
|
||||
!command ||
|
||||
command.status !== "claimed" ||
|
||||
command.claimedBySessionId !== args.sessionId
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const timestamp = Date.now();
|
||||
await ctx.db.patch("daemonCommands", command._id, {
|
||||
status: "running",
|
||||
startedAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
});
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
export const succeed = mutation({
|
||||
args: {
|
||||
commandId: v.id("daemonCommands"),
|
||||
sessionId: v.string(),
|
||||
result: v.any(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const command = await ctx.db.get("daemonCommands", args.commandId);
|
||||
if (!command || command.claimedBySessionId !== args.sessionId) {
|
||||
return false;
|
||||
}
|
||||
const timestamp = Date.now();
|
||||
await ctx.db.patch("daemonCommands", command._id, {
|
||||
status: "succeeded",
|
||||
result: args.result,
|
||||
completedAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
leaseExpiresAt: undefined,
|
||||
});
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
export const fail = mutation({
|
||||
args: {
|
||||
commandId: v.id("daemonCommands"),
|
||||
sessionId: v.string(),
|
||||
error: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const command = await ctx.db.get("daemonCommands", args.commandId);
|
||||
if (!command || command.claimedBySessionId !== args.sessionId) {
|
||||
return false;
|
||||
}
|
||||
const timestamp = Date.now();
|
||||
await ctx.db.patch("daemonCommands", command._id, {
|
||||
status: "failed",
|
||||
error: args.error,
|
||||
completedAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
leaseExpiresAt: undefined,
|
||||
});
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
export const cancel = mutation({
|
||||
args: { commandId: v.id("daemonCommands") },
|
||||
handler: async (ctx, args) => {
|
||||
const command = await ctx.db.get("daemonCommands", args.commandId);
|
||||
if (
|
||||
!command ||
|
||||
["succeeded", "failed", "cancelled"].includes(command.status)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const timestamp = Date.now();
|
||||
await ctx.db.patch("daemonCommands", command._id, {
|
||||
status: "cancelled",
|
||||
completedAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
leaseExpiresAt: undefined,
|
||||
});
|
||||
return true;
|
||||
},
|
||||
});
|
||||
107
packages/backend/convex/daemonRuntime.ts
Normal file
107
packages/backend/convex/daemonRuntime.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { v } from "convex/values";
|
||||
|
||||
import { mutation, query } from "./_generated/server";
|
||||
|
||||
export const connect = mutation({
|
||||
args: {
|
||||
daemonId: v.string(),
|
||||
sessionId: v.string(),
|
||||
hostname: v.string(),
|
||||
platform: v.string(),
|
||||
architecture: v.string(),
|
||||
version: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const existing = await ctx.db
|
||||
.query("daemonPresence")
|
||||
.withIndex("by_sessionId", (q) => q.eq("sessionId", args.sessionId))
|
||||
.unique();
|
||||
const timestamp = Date.now();
|
||||
if (existing) {
|
||||
await ctx.db.patch("daemonPresence", existing._id, {
|
||||
status: "online",
|
||||
lastHeartbeatAt: timestamp,
|
||||
});
|
||||
return existing._id;
|
||||
}
|
||||
return await ctx.db.insert("daemonPresence", {
|
||||
...args,
|
||||
status: "online",
|
||||
startedAt: timestamp,
|
||||
lastHeartbeatAt: timestamp,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const heartbeat = mutation({
|
||||
args: {
|
||||
daemonId: v.string(),
|
||||
sessionId: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const presence = await ctx.db
|
||||
.query("daemonPresence")
|
||||
.withIndex("by_sessionId", (q) => q.eq("sessionId", args.sessionId))
|
||||
.unique();
|
||||
if (!presence || presence.daemonId !== args.daemonId) {
|
||||
return false;
|
||||
}
|
||||
await ctx.db.patch("daemonPresence", presence._id, {
|
||||
status: "online",
|
||||
lastHeartbeatAt: Date.now(),
|
||||
});
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
export const disconnect = mutation({
|
||||
args: {
|
||||
daemonId: v.string(),
|
||||
sessionId: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const presence = await ctx.db
|
||||
.query("daemonPresence")
|
||||
.withIndex("by_sessionId", (q) => q.eq("sessionId", args.sessionId))
|
||||
.unique();
|
||||
if (!presence || presence.daemonId !== args.daemonId) {
|
||||
return false;
|
||||
}
|
||||
await ctx.db.patch("daemonPresence", presence._id, {
|
||||
status: "offline",
|
||||
lastHeartbeatAt: Date.now(),
|
||||
});
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
export const recordEvent = mutation({
|
||||
args: {
|
||||
daemonId: v.string(),
|
||||
commandId: v.optional(v.id("daemonCommands")),
|
||||
kind: v.string(),
|
||||
data: v.any(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.db.insert("daemonCommandEvents", {
|
||||
...args,
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const listEvents = query({
|
||||
args: {
|
||||
daemonId: v.string(),
|
||||
limit: v.optional(v.number()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.db
|
||||
.query("daemonCommandEvents")
|
||||
.withIndex("by_daemonId_and_createdAt", (q) =>
|
||||
q.eq("daemonId", args.daemonId)
|
||||
)
|
||||
.order("desc")
|
||||
.take(Math.min(args.limit ?? 100, 500));
|
||||
},
|
||||
});
|
||||
60
packages/backend/convex/daemons.ts
Normal file
60
packages/backend/convex/daemons.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { v } from "convex/values";
|
||||
|
||||
import { mutation, query } from "./_generated/server";
|
||||
|
||||
const now = () => Date.now();
|
||||
|
||||
export const upsert = mutation({
|
||||
args: {
|
||||
daemonId: v.string(),
|
||||
name: v.string(),
|
||||
desiredState: v.union(v.literal("online"), v.literal("offline")),
|
||||
agentOsConfig: v.any(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const existing = await ctx.db
|
||||
.query("daemonDefinitions")
|
||||
.withIndex("by_daemonId", (q) => q.eq("daemonId", args.daemonId))
|
||||
.unique();
|
||||
const timestamp = now();
|
||||
if (existing) {
|
||||
await ctx.db.patch("daemonDefinitions", existing._id, {
|
||||
name: args.name,
|
||||
desiredState: args.desiredState,
|
||||
agentOsConfig: args.agentOsConfig,
|
||||
configVersion: existing.configVersion + 1,
|
||||
updatedAt: timestamp,
|
||||
});
|
||||
return existing._id;
|
||||
}
|
||||
return await ctx.db.insert("daemonDefinitions", {
|
||||
...args,
|
||||
configVersion: 1,
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const get = query({
|
||||
args: { daemonId: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
const definition = await ctx.db
|
||||
.query("daemonDefinitions")
|
||||
.withIndex("by_daemonId", (q) => q.eq("daemonId", args.daemonId))
|
||||
.unique();
|
||||
const presence = await ctx.db
|
||||
.query("daemonPresence")
|
||||
.withIndex("by_daemonId", (q) => q.eq("daemonId", args.daemonId))
|
||||
.order("desc")
|
||||
.first();
|
||||
return { definition, presence };
|
||||
},
|
||||
});
|
||||
|
||||
export const list = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
return await ctx.db.query("daemonDefinitions").order("desc").collect();
|
||||
},
|
||||
});
|
||||
@@ -2,6 +2,66 @@ import { defineSchema, defineTable } from "convex/server";
|
||||
import { v } from "convex/values";
|
||||
|
||||
export default defineSchema({
|
||||
daemonDefinitions: defineTable({
|
||||
daemonId: v.string(),
|
||||
name: v.string(),
|
||||
desiredState: v.union(v.literal("online"), v.literal("offline")),
|
||||
agentOsConfig: v.any(),
|
||||
configVersion: v.number(),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
}).index("by_daemonId", ["daemonId"]),
|
||||
daemonPresence: defineTable({
|
||||
daemonId: v.string(),
|
||||
sessionId: v.string(),
|
||||
status: v.union(
|
||||
v.literal("online"),
|
||||
v.literal("draining"),
|
||||
v.literal("offline")
|
||||
),
|
||||
hostname: v.string(),
|
||||
platform: v.string(),
|
||||
architecture: v.string(),
|
||||
version: v.string(),
|
||||
startedAt: v.number(),
|
||||
lastHeartbeatAt: v.number(),
|
||||
})
|
||||
.index("by_daemonId", ["daemonId"])
|
||||
.index("by_sessionId", ["sessionId"]),
|
||||
daemonCommands: defineTable({
|
||||
daemonId: v.string(),
|
||||
method: v.string(),
|
||||
args: v.any(),
|
||||
actorKey: v.array(v.string()),
|
||||
status: v.union(
|
||||
v.literal("queued"),
|
||||
v.literal("claimed"),
|
||||
v.literal("running"),
|
||||
v.literal("succeeded"),
|
||||
v.literal("failed"),
|
||||
v.literal("cancelled")
|
||||
),
|
||||
attempts: v.number(),
|
||||
claimedBySessionId: v.optional(v.string()),
|
||||
leaseExpiresAt: v.optional(v.number()),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
startedAt: v.optional(v.number()),
|
||||
completedAt: v.optional(v.number()),
|
||||
result: v.optional(v.any()),
|
||||
error: v.optional(v.string()),
|
||||
})
|
||||
.index("by_daemonId_and_status", ["daemonId", "status"])
|
||||
.index("by_status_and_leaseExpiresAt", ["status", "leaseExpiresAt"]),
|
||||
daemonCommandEvents: defineTable({
|
||||
daemonId: v.string(),
|
||||
commandId: v.optional(v.id("daemonCommands")),
|
||||
kind: v.string(),
|
||||
data: v.any(),
|
||||
createdAt: v.number(),
|
||||
})
|
||||
.index("by_daemonId_and_createdAt", ["daemonId", "createdAt"])
|
||||
.index("by_commandId_and_createdAt", ["commandId", "createdAt"]),
|
||||
todos: defineTable({
|
||||
text: v.string(),
|
||||
completed: v.boolean(),
|
||||
|
||||
3
packages/env/package.json
vendored
3
packages/env/package.json
vendored
@@ -5,7 +5,8 @@
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./web": "./src/web.ts",
|
||||
"./native": "./src/native.ts"
|
||||
"./native": "./src/native.ts",
|
||||
"./server": "./src/server.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@t3-oss/env-core": "^0.13.11",
|
||||
|
||||
22
packages/env/src/server.ts
vendored
Normal file
22
packages/env/src/server.ts
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
import { createEnv } from "@t3-oss/env-core";
|
||||
import { z } from "zod";
|
||||
|
||||
const convexUrlSchema = (exampleHost: string) =>
|
||||
z.url().refine((url) => new URL(url).hostname !== exampleHost, {
|
||||
message: `Replace the ${exampleHost} placeholder before running the app`,
|
||||
});
|
||||
|
||||
export const env = createEnv({
|
||||
server: {
|
||||
CONVEX_URL: convexUrlSchema("example.convex.cloud"),
|
||||
DAEMON_ID: z.string().min(1),
|
||||
DAEMON_NAME: z.string().min(1).optional(),
|
||||
DAEMON_VERSION: z.string().default("0.0.0"),
|
||||
DAEMON_HEARTBEAT_MS: z.coerce.number().int().positive().default(15_000),
|
||||
DAEMON_COMMAND_LEASE_MS: z.coerce.number().int().positive().default(60_000),
|
||||
RIVET_ENDPOINT: z.url().optional(),
|
||||
},
|
||||
runtimeEnv: process.env,
|
||||
skipValidation: process.env.SKIP_ENV_VALIDATION === "true",
|
||||
emptyStringAsUndefined: true,
|
||||
});
|
||||
22
packages/primitives/package.json
Normal file
22
packages/primitives/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@code/primitives",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./agent-os": "./src/agent-os.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"check-types": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rivet-dev/agentos": "^0.2.7",
|
||||
"effect": "4.0.0-beta.99"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@code/config": "workspace:*",
|
||||
"@types/node": "^22.13.14",
|
||||
"typescript": "^6"
|
||||
}
|
||||
}
|
||||
64
packages/primitives/src/agent-os.ts
Normal file
64
packages/primitives/src/agent-os.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
agentOS as createAgentOsActor,
|
||||
type AgentOSConfigInput,
|
||||
} from "@rivet-dev/agentos";
|
||||
import { Context, Effect, Layer, Schema } from "effect";
|
||||
|
||||
export class AgentOsError extends Schema.TaggedErrorClass<AgentOsError>()(
|
||||
"AgentOsError",
|
||||
{
|
||||
cause: Schema.Defect(),
|
||||
}
|
||||
) {}
|
||||
|
||||
export interface AgentOsOptions {
|
||||
readonly config?: AgentOSConfigInput<undefined>;
|
||||
}
|
||||
|
||||
export class AgentOs extends Context.Service<
|
||||
AgentOs,
|
||||
{
|
||||
readonly createActor: (
|
||||
config?: AgentOSConfigInput<undefined>
|
||||
) => Effect.Effect<
|
||||
ReturnType<typeof createAgentOsActor<undefined>>,
|
||||
AgentOsError
|
||||
>;
|
||||
}
|
||||
>()("@code/primitives/agent-os/AgentOs") {
|
||||
static readonly layer = Layer.succeed(
|
||||
AgentOs,
|
||||
AgentOs.of({
|
||||
createActor: Effect.fn("AgentOs.createActor")(function* (
|
||||
config: AgentOSConfigInput<undefined> = {}
|
||||
) {
|
||||
return yield* Effect.try({
|
||||
try: () => createAgentOsActor<undefined>(config),
|
||||
catch: (cause) => new AgentOsError({ cause }),
|
||||
});
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export const makeAgentOsLayer = (options: AgentOsOptions = {}) =>
|
||||
Layer.succeed(
|
||||
AgentOs,
|
||||
AgentOs.of({
|
||||
createActor: Effect.fn("AgentOs.createActor")(function* (
|
||||
config: AgentOSConfigInput<undefined> = options.config ?? {}
|
||||
) {
|
||||
return yield* Effect.try({
|
||||
try: () => createAgentOsActor<undefined>(config),
|
||||
catch: (cause) => new AgentOsError({ cause }),
|
||||
});
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
export const createAgentOsActorEffect = Effect.fn("createAgentOsActorEffect")(
|
||||
function* (config?: AgentOSConfigInput<undefined>) {
|
||||
const agentOs = yield* AgentOs;
|
||||
return yield* agentOs.createActor(config);
|
||||
}
|
||||
);
|
||||
1
packages/primitives/src/index.ts
Normal file
1
packages/primitives/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./agent-os";
|
||||
5
packages/primitives/tsconfig.json
Normal file
5
packages/primitives/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "@code/config/tsconfig.base.json",
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user