stagent 0.9.0 → 0.9.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stagent",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "AI Business Operating System — run your business with AI agents. Local-first, multi-provider, governed.",
5
5
  "keywords": [
6
6
  "ai",
@@ -3,10 +3,7 @@ import { createClient } from "@supabase/supabase-js";
3
3
  import { licenseManager } from "@/lib/license/manager";
4
4
  import { validateLicenseWithCloud } from "@/lib/license/cloud-validation";
5
5
  import { sendUpgradeConfirmation } from "@/lib/billing/email";
6
-
7
- const DEFAULT_SUPABASE_URL = "https://yznantjbmacbllhcyzwc.supabase.co";
8
- const DEFAULT_SUPABASE_ANON_KEY =
9
- "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl6bmFudGpibWFjYmxsaGN5endjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI1MDg1ODMsImV4cCI6MjA4ODA4NDU4M30.i-P7MXpR1_emBjhUkzbFeSX7fgjgPDv90_wkqF7sW3Y";
6
+ import { getSupabaseUrl, getSupabaseAnonKey } from "@/lib/cloud/supabase-client";
10
7
 
11
8
  /**
12
9
  * GET /auth/callback
@@ -30,10 +27,7 @@ export async function GET(req: NextRequest) {
30
27
  return NextResponse.redirect(new URL("/settings?auth=error", req.url));
31
28
  }
32
29
 
33
- const url = process.env.NEXT_PUBLIC_SUPABASE_URL || DEFAULT_SUPABASE_URL;
34
- const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || DEFAULT_SUPABASE_ANON_KEY;
35
-
36
- const supabase = createClient(url, anonKey, {
30
+ const supabase = createClient(getSupabaseUrl(), getSupabaseAnonKey(), {
37
31
  auth: { persistSession: false },
38
32
  });
39
33
 
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Node.js-only instrumentation startup.
3
+ * Imported dynamically from instrumentation.ts with a bundler-ignore comment
4
+ * so the Edge runtime never analyzes this module's dependency tree.
5
+ */
6
+ export async function registerNode() {
7
+ // License manager — initialize from DB (creates default row if needed)
8
+ const { licenseManager } = await import("@/lib/license/manager");
9
+ licenseManager.initialize();
10
+ licenseManager.startValidationTimer();
11
+
12
+ const { startScheduler } = await import("@/lib/schedules/scheduler");
13
+ startScheduler();
14
+
15
+ const { startChannelPoller } = await import("@/lib/channels/poller");
16
+ startChannelPoller();
17
+
18
+ const { startAutoBackup } = await import("@/lib/snapshots/auto-backup");
19
+ startAutoBackup();
20
+
21
+ // History retention cleanup — prunes old agent_logs and usage_ledger
22
+ // based on tier retention limit (Community: 30 days)
23
+ const CLEANUP_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours
24
+
25
+ async function cleanup() {
26
+ const retentionDays = licenseManager.getLimit("historyRetentionDays");
27
+ if (!Number.isFinite(retentionDays)) return; // Unlimited retention
28
+
29
+ const { db } = await import("@/lib/db");
30
+ const { agentLogs, usageLedger } = await import("@/lib/db/schema");
31
+ const { lt } = await import("drizzle-orm");
32
+
33
+ const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000);
34
+ db.delete(agentLogs).where(lt(agentLogs.timestamp, cutoff)).run();
35
+ db.delete(usageLedger).where(lt(usageLedger.startedAt, cutoff)).run();
36
+ }
37
+
38
+ cleanup().catch(() => {});
39
+ setInterval(() => cleanup().catch(() => {}), CLEANUP_INTERVAL);
40
+
41
+ // Telemetry batch flush (opt-in, every 5 minutes)
42
+ const { startTelemetryFlush } = await import("@/lib/telemetry/queue");
43
+ startTelemetryFlush();
44
+ }
@@ -1,47 +1,10 @@
1
1
  export async function register() {
2
- // Only start background services on the server (not during build or edge)
3
2
  if (process.env.NEXT_RUNTIME === "nodejs") {
4
- // License manager initialize from DB (creates default row if needed)
5
- const { licenseManager } = await import("@/lib/license/manager");
6
- licenseManager.initialize();
7
- licenseManager.startValidationTimer();
8
-
9
- const { startScheduler } = await import("@/lib/schedules/scheduler");
10
- startScheduler();
11
-
12
- const { startChannelPoller } = await import("@/lib/channels/poller");
13
- startChannelPoller();
14
-
15
- const { startAutoBackup } = await import("@/lib/snapshots/auto-backup");
16
- startAutoBackup();
17
-
18
- // History retention cleanup — prunes old agent_logs and usage_ledger
19
- // based on tier retention limit (Community: 30 days)
20
- startHistoryCleanup(licenseManager);
21
-
22
- // Telemetry batch flush (opt-in, every 5 minutes)
23
- const { startTelemetryFlush } = await import("@/lib/telemetry/queue");
24
- startTelemetryFlush();
3
+ // Single dynamic import with bundler-ignore so Turbopack's Edge analyzer
4
+ // never follows this module's Node.js dependency tree.
5
+ const { registerNode } = await import(
6
+ /* webpackIgnore: true */ "./instrumentation.node"
7
+ );
8
+ await registerNode();
25
9
  }
26
10
  }
27
-
28
- async function startHistoryCleanup(licenseManager: { getLimit: (r: "historyRetentionDays") => number }) {
29
- const CLEANUP_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours
30
-
31
- async function cleanup() {
32
- const retentionDays = licenseManager.getLimit("historyRetentionDays");
33
- if (!Number.isFinite(retentionDays)) return; // Unlimited retention
34
-
35
- const { db } = await import("@/lib/db");
36
- const { agentLogs, usageLedger } = await import("@/lib/db/schema");
37
- const { lt } = await import("drizzle-orm");
38
-
39
- const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000);
40
- db.delete(agentLogs).where(lt(agentLogs.timestamp, cutoff)).run();
41
- db.delete(usageLedger).where(lt(usageLedger.startedAt, cutoff)).run();
42
- }
43
-
44
- // Run once at startup, then daily
45
- cleanup().catch(() => {});
46
- setInterval(() => cleanup().catch(() => {}), CLEANUP_INTERVAL);
47
- }
@@ -1,4 +1,5 @@
1
1
  import fs from "node:fs";
2
+ import { homedir } from "node:os";
2
3
  import path from "node:path";
3
4
  import yaml from "js-yaml";
4
5
  import { ProfileConfigSchema } from "@/lib/validators/profile";
@@ -30,7 +31,7 @@ function getBuiltinsDir(): string {
30
31
  }
31
32
 
32
33
  const SKILLS_DIR = path.join(
33
- process.env.HOME ?? process.env.USERPROFILE ?? ".",
34
+ process.env.HOME ?? process.env.USERPROFILE ?? homedir(),
34
35
  ".claude",
35
36
  "skills"
36
37
  );
@@ -6,7 +6,7 @@
6
6
  * the cloud backend is not configured.
7
7
  */
8
8
 
9
- import { isCloudConfigured } from "@/lib/cloud/supabase-client";
9
+ import { isCloudConfigured, getSupabaseUrl, getSupabaseAnonKey } from "@/lib/cloud/supabase-client";
10
10
 
11
11
  async function sendEmail(
12
12
  template: string,
@@ -15,8 +15,8 @@ async function sendEmail(
15
15
  ): Promise<void> {
16
16
  if (!isCloudConfigured()) return;
17
17
 
18
- const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
19
- const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
18
+ const supabaseUrl = getSupabaseUrl();
19
+ const anonKey = getSupabaseAnonKey();
20
20
 
21
21
  try {
22
22
  await fetch(`${supabaseUrl}/functions/v1/send-email`, {
@@ -6,7 +6,7 @@
6
6
  * Edge Functions to create Checkout Sessions and Portal Sessions.
7
7
  */
8
8
 
9
- import { isCloudConfigured } from "@/lib/cloud/supabase-client";
9
+ import { isCloudConfigured, getSupabaseUrl, getSupabaseAnonKey } from "@/lib/cloud/supabase-client";
10
10
  import { getProductForTier } from "./products";
11
11
  import type { LicenseTier } from "@/lib/license/tier-limits";
12
12
  import type { BillingInterval } from "./products";
@@ -30,8 +30,8 @@ export async function createCheckoutSession(
30
30
  }
31
31
 
32
32
  const priceId = product.prices[billingPeriod].id;
33
- const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
34
- const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
33
+ const supabaseUrl = getSupabaseUrl();
34
+ const anonKey = getSupabaseAnonKey();
35
35
 
36
36
  try {
37
37
  const response = await fetch(
@@ -72,8 +72,8 @@ export async function createPortalSession(
72
72
  return { error: "Cloud backend not configured" };
73
73
  }
74
74
 
75
- const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
76
- const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
75
+ const supabaseUrl = getSupabaseUrl();
76
+ const anonKey = getSupabaseAnonKey();
77
77
 
78
78
  try {
79
79
  const response = await fetch(
@@ -9,10 +9,7 @@
9
9
  */
10
10
 
11
11
  import { createClient, type SupabaseClient } from "@supabase/supabase-js";
12
-
13
- const DEFAULT_SUPABASE_URL = "https://yznantjbmacbllhcyzwc.supabase.co";
14
- const DEFAULT_SUPABASE_ANON_KEY =
15
- "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl6bmFudGpibWFjYmxsaGN5endjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI1MDg1ODMsImV4cCI6MjA4ODA4NDU4M30.i-P7MXpR1_emBjhUkzbFeSX7fgjgPDv90_wkqF7sW3Y";
12
+ import { getSupabaseUrl, getSupabaseAnonKey } from "@/lib/cloud/supabase-client";
16
13
 
17
14
  let browserClient: SupabaseClient | null = null;
18
15
 
@@ -23,10 +20,7 @@ let browserClient: SupabaseClient | null = null;
23
20
  export function getSupabaseBrowserClient(): SupabaseClient {
24
21
  if (browserClient) return browserClient;
25
22
 
26
- const url = process.env.NEXT_PUBLIC_SUPABASE_URL || DEFAULT_SUPABASE_URL;
27
- const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || DEFAULT_SUPABASE_ANON_KEY;
28
-
29
- browserClient = createClient(url, anonKey, {
23
+ browserClient = createClient(getSupabaseUrl(), getSupabaseAnonKey(), {
30
24
  auth: {
31
25
  autoRefreshToken: true,
32
26
  persistSession: true,
@@ -15,6 +15,16 @@ const DEFAULT_SUPABASE_URL = "https://yznantjbmacbllhcyzwc.supabase.co";
15
15
  const DEFAULT_SUPABASE_ANON_KEY =
16
16
  "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl6bmFudGpibWFjYmxsaGN5endjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI1MDg1ODMsImV4cCI6MjA4ODA4NDU4M30.i-P7MXpR1_emBjhUkzbFeSX7fgjgPDv90_wkqF7sW3Y";
17
17
 
18
+ /** Resolved Supabase URL (env override or production default) */
19
+ export function getSupabaseUrl(): string {
20
+ return process.env.NEXT_PUBLIC_SUPABASE_URL || DEFAULT_SUPABASE_URL;
21
+ }
22
+
23
+ /** Resolved Supabase anon key (env override or production default) */
24
+ export function getSupabaseAnonKey(): string {
25
+ return process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || DEFAULT_SUPABASE_ANON_KEY;
26
+ }
27
+
18
28
  let client: SupabaseClient | null = null;
19
29
  let initialized = false;
20
30
 
@@ -27,10 +37,7 @@ export function getSupabaseClient(): SupabaseClient | null {
27
37
  if (initialized) return client;
28
38
  initialized = true;
29
39
 
30
- const url = process.env.NEXT_PUBLIC_SUPABASE_URL || DEFAULT_SUPABASE_URL;
31
- const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || DEFAULT_SUPABASE_ANON_KEY;
32
-
33
- client = createClient(url, anonKey, {
40
+ client = createClient(getSupabaseUrl(), getSupabaseAnonKey(), {
34
41
  auth: {
35
42
  autoRefreshToken: true,
36
43
  persistSession: false, // Server-side — no browser session
@@ -6,13 +6,9 @@
6
6
  * defaults as supabase-client.ts (production backend by default).
7
7
  */
8
8
 
9
- import { isCloudConfigured } from "@/lib/cloud/supabase-client";
9
+ import { isCloudConfigured, getSupabaseUrl, getSupabaseAnonKey } from "@/lib/cloud/supabase-client";
10
10
  import type { LicenseTier } from "./tier-limits";
11
11
 
12
- const DEFAULT_SUPABASE_URL = "https://yznantjbmacbllhcyzwc.supabase.co";
13
- const DEFAULT_SUPABASE_ANON_KEY =
14
- "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl6bmFudGpibWFjYmxsaGN5endjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI1MDg1ODMsImV4cCI6MjA4ODA4NDU4M30.i-P7MXpR1_emBjhUkzbFeSX7fgjgPDv90_wkqF7sW3Y";
15
-
16
12
  export interface CloudValidationResult {
17
13
  valid: boolean;
18
14
  tier: LicenseTier;
@@ -31,8 +27,8 @@ export async function validateLicenseWithCloud(
31
27
  return { valid: false, tier: "community", error: "Cloud disabled or no email" };
32
28
  }
33
29
 
34
- const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || DEFAULT_SUPABASE_URL;
35
- const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || DEFAULT_SUPABASE_ANON_KEY;
30
+ const supabaseUrl = getSupabaseUrl();
31
+ const anonKey = getSupabaseAnonKey();
36
32
 
37
33
  try {
38
34
  const response = await fetch(`${supabaseUrl}/functions/v1/validate-license`, {
@@ -4,7 +4,7 @@
4
4
  * Writes go directly to Supabase with RLS.
5
5
  */
6
6
 
7
- import { getSupabaseClient, isCloudConfigured } from "@/lib/cloud/supabase-client";
7
+ import { getSupabaseClient, isCloudConfigured, getSupabaseUrl, getSupabaseAnonKey } from "@/lib/cloud/supabase-client";
8
8
 
9
9
  export interface MarketplaceBlueprint {
10
10
  id: string;
@@ -36,8 +36,8 @@ export async function browseBlueprints(
36
36
  return { blueprints: [], total: 0, page, limit: 20 };
37
37
  }
38
38
 
39
- const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
40
- const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
39
+ const supabaseUrl = getSupabaseUrl();
40
+ const anonKey = getSupabaseAnonKey();
41
41
 
42
42
  const params = new URLSearchParams({ page: String(page) });
43
43
  if (category) params.set("category", category);
@@ -12,7 +12,7 @@ import { readFileSync, writeFileSync, existsSync, copyFileSync } from "fs";
12
12
  import { join } from "path";
13
13
  import { tmpdir } from "os";
14
14
  import { getStagentDataDir } from "@/lib/utils/stagent-paths";
15
- import { getSupabaseClient } from "@/lib/cloud/supabase-client";
15
+ import { getSupabaseClient, getSupabaseUrl, getSupabaseAnonKey } from "@/lib/cloud/supabase-client";
16
16
  import { sqlite } from "@/lib/db";
17
17
 
18
18
  const SYNC_VERSION = Buffer.from([0, 0, 0, 1]); // Version 1
@@ -89,9 +89,7 @@ export async function exportAndUpload(
89
89
  // If an access token is provided, create an authenticated client for Storage RLS
90
90
  if (accessToken) {
91
91
  const { createClient } = await import("@supabase/supabase-js");
92
- const url = process.env.NEXT_PUBLIC_SUPABASE_URL || "https://yznantjbmacbllhcyzwc.supabase.co";
93
- const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl6bmFudGpibWFjYmxsaGN5endjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI1MDg1ODMsImV4cCI6MjA4ODA4NDU4M30.i-P7MXpR1_emBjhUkzbFeSX7fgjgPDv90_wkqF7sW3Y";
94
- supabase = createClient(url, anonKey, {
92
+ supabase = createClient(getSupabaseUrl(), getSupabaseAnonKey(), {
95
93
  global: { headers: { Authorization: `Bearer ${accessToken}` } },
96
94
  auth: { persistSession: false },
97
95
  });
@@ -9,7 +9,7 @@
9
9
  * No-op if Supabase is not configured.
10
10
  */
11
11
 
12
- import { isCloudConfigured } from "@/lib/cloud/supabase-client";
12
+ import { isCloudConfigured, getSupabaseUrl, getSupabaseAnonKey } from "@/lib/cloud/supabase-client";
13
13
  import { getSettingSync, setSetting } from "@/lib/settings/helpers";
14
14
 
15
15
  const SESSION_KEY = "conversion.sessionId";
@@ -49,10 +49,8 @@ export function trackConversionEvent(
49
49
  if (!isCloudConfigured()) return;
50
50
 
51
51
  const sessionId = getSessionId();
52
- const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
53
- const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
54
-
55
- if (!supabaseUrl || !anonKey) return;
52
+ const supabaseUrl = getSupabaseUrl();
53
+ const anonKey = getSupabaseAnonKey();
56
54
 
57
55
  // Fire-and-forget — don't await, don't block
58
56
  fetch(`${supabaseUrl}/functions/v1/conversion-ingest`, {
@@ -11,7 +11,7 @@
11
11
 
12
12
  import { getSettingSync, setSetting } from "@/lib/settings/helpers";
13
13
  import { SETTINGS_KEYS } from "@/lib/constants/settings";
14
- import { isCloudConfigured } from "@/lib/cloud/supabase-client";
14
+ import { isCloudConfigured, getSupabaseUrl, getSupabaseAnonKey } from "@/lib/cloud/supabase-client";
15
15
 
16
16
  const MAX_BATCH_SIZE = 200;
17
17
  const FLUSH_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
@@ -75,8 +75,8 @@ export async function flushTelemetryBatch(): Promise<void> {
75
75
  const batch = loadBatch();
76
76
  if (batch.length === 0) return;
77
77
 
78
- const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
79
- const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
78
+ const supabaseUrl = getSupabaseUrl();
79
+ const anonKey = getSupabaseAnonKey();
80
80
 
81
81
  try {
82
82
  const res = await fetch(`${supabaseUrl}/functions/v1/telemetry-ingest`, {