@build-astron-co/nimbus 0.2.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.
- package/LICENSE +21 -0
- package/README.md +628 -0
- package/bin/nimbus +38 -0
- package/package.json +80 -0
- package/src/__tests__/app.test.ts +76 -0
- package/src/__tests__/audit.test.ts +877 -0
- package/src/__tests__/circuit-breaker.test.ts +116 -0
- package/src/__tests__/cli-run.test.ts +115 -0
- package/src/__tests__/context-manager.test.ts +502 -0
- package/src/__tests__/context.test.ts +242 -0
- package/src/__tests__/enterprise.test.ts +401 -0
- package/src/__tests__/generator.test.ts +433 -0
- package/src/__tests__/hooks.test.ts +582 -0
- package/src/__tests__/init.test.ts +436 -0
- package/src/__tests__/intent-parser.test.ts +229 -0
- package/src/__tests__/llm-router.test.ts +209 -0
- package/src/__tests__/lsp.test.ts +293 -0
- package/src/__tests__/modes.test.ts +336 -0
- package/src/__tests__/permissions.test.ts +338 -0
- package/src/__tests__/serve.test.ts +275 -0
- package/src/__tests__/sessions.test.ts +227 -0
- package/src/__tests__/sharing.test.ts +288 -0
- package/src/__tests__/snapshots.test.ts +581 -0
- package/src/__tests__/state-db.test.ts +334 -0
- package/src/__tests__/stream-with-tools.test.ts +732 -0
- package/src/__tests__/subagents.test.ts +176 -0
- package/src/__tests__/system-prompt.test.ts +169 -0
- package/src/__tests__/tool-converter.test.ts +256 -0
- package/src/__tests__/tool-schemas.test.ts +397 -0
- package/src/__tests__/tools.test.ts +143 -0
- package/src/__tests__/version.test.ts +49 -0
- package/src/agent/compaction-agent.ts +227 -0
- package/src/agent/context-manager.ts +435 -0
- package/src/agent/context.ts +427 -0
- package/src/agent/deploy-preview.ts +426 -0
- package/src/agent/index.ts +68 -0
- package/src/agent/loop.ts +717 -0
- package/src/agent/modes.ts +429 -0
- package/src/agent/permissions.ts +466 -0
- package/src/agent/subagents/base.ts +116 -0
- package/src/agent/subagents/cost.ts +51 -0
- package/src/agent/subagents/explore.ts +42 -0
- package/src/agent/subagents/general.ts +54 -0
- package/src/agent/subagents/index.ts +102 -0
- package/src/agent/subagents/infra.ts +59 -0
- package/src/agent/subagents/security.ts +69 -0
- package/src/agent/system-prompt.ts +436 -0
- package/src/app.ts +122 -0
- package/src/audit/activity-log.ts +290 -0
- package/src/audit/compliance-checker.ts +540 -0
- package/src/audit/cost-tracker.ts +318 -0
- package/src/audit/index.ts +23 -0
- package/src/audit/security-scanner.ts +596 -0
- package/src/auth/guard.ts +75 -0
- package/src/auth/index.ts +56 -0
- package/src/auth/oauth.ts +455 -0
- package/src/auth/providers.ts +470 -0
- package/src/auth/sso.ts +113 -0
- package/src/auth/store.ts +505 -0
- package/src/auth/types.ts +187 -0
- package/src/build.ts +141 -0
- package/src/cli/index.ts +16 -0
- package/src/cli/init.ts +854 -0
- package/src/cli/openapi-spec.ts +356 -0
- package/src/cli/run.ts +237 -0
- package/src/cli/serve-auth.ts +80 -0
- package/src/cli/serve.ts +462 -0
- package/src/cli/web.ts +67 -0
- package/src/cli.ts +1417 -0
- package/src/clients/core-engine-client.ts +227 -0
- package/src/clients/enterprise-client.ts +334 -0
- package/src/clients/generator-client.ts +351 -0
- package/src/clients/git-client.ts +627 -0
- package/src/clients/github-client.ts +410 -0
- package/src/clients/helm-client.ts +504 -0
- package/src/clients/index.ts +80 -0
- package/src/clients/k8s-client.ts +497 -0
- package/src/clients/llm-client.ts +161 -0
- package/src/clients/rest-client.ts +130 -0
- package/src/clients/service-discovery.ts +33 -0
- package/src/clients/terraform-client.ts +482 -0
- package/src/clients/tools-client.ts +1843 -0
- package/src/clients/ws-client.ts +115 -0
- package/src/commands/analyze/index.ts +352 -0
- package/src/commands/apply/helm.ts +473 -0
- package/src/commands/apply/index.ts +213 -0
- package/src/commands/apply/k8s.ts +454 -0
- package/src/commands/apply/terraform.ts +582 -0
- package/src/commands/ask.ts +167 -0
- package/src/commands/audit/index.ts +238 -0
- package/src/commands/auth-cloud.ts +294 -0
- package/src/commands/auth-list.ts +134 -0
- package/src/commands/auth-profile.ts +121 -0
- package/src/commands/auth-status.ts +141 -0
- package/src/commands/aws/ec2.ts +501 -0
- package/src/commands/aws/iam.ts +397 -0
- package/src/commands/aws/index.ts +133 -0
- package/src/commands/aws/lambda.ts +396 -0
- package/src/commands/aws/rds.ts +439 -0
- package/src/commands/aws/s3.ts +439 -0
- package/src/commands/aws/vpc.ts +393 -0
- package/src/commands/aws-discover.ts +649 -0
- package/src/commands/aws-terraform.ts +805 -0
- package/src/commands/azure/aks.ts +376 -0
- package/src/commands/azure/functions.ts +253 -0
- package/src/commands/azure/index.ts +116 -0
- package/src/commands/azure/storage.ts +478 -0
- package/src/commands/azure/vm.ts +355 -0
- package/src/commands/billing/index.ts +256 -0
- package/src/commands/chat.ts +314 -0
- package/src/commands/config.ts +346 -0
- package/src/commands/cost/cloud-cost-estimator.ts +266 -0
- package/src/commands/cost/estimator.ts +79 -0
- package/src/commands/cost/index.ts +594 -0
- package/src/commands/cost/parsers/terraform.ts +273 -0
- package/src/commands/cost/parsers/types.ts +25 -0
- package/src/commands/cost/pricing/aws.ts +544 -0
- package/src/commands/cost/pricing/azure.ts +499 -0
- package/src/commands/cost/pricing/gcp.ts +396 -0
- package/src/commands/cost/pricing/index.ts +40 -0
- package/src/commands/demo.ts +250 -0
- package/src/commands/doctor.ts +794 -0
- package/src/commands/drift/index.ts +439 -0
- package/src/commands/explain.ts +277 -0
- package/src/commands/feedback.ts +389 -0
- package/src/commands/fix.ts +324 -0
- package/src/commands/fs/index.ts +402 -0
- package/src/commands/gcp/compute.ts +325 -0
- package/src/commands/gcp/functions.ts +271 -0
- package/src/commands/gcp/gke.ts +438 -0
- package/src/commands/gcp/iam.ts +344 -0
- package/src/commands/gcp/index.ts +129 -0
- package/src/commands/gcp/storage.ts +284 -0
- package/src/commands/generate-helm.ts +1249 -0
- package/src/commands/generate-k8s.ts +1560 -0
- package/src/commands/generate-terraform.ts +1460 -0
- package/src/commands/gh/index.ts +863 -0
- package/src/commands/git/index.ts +1343 -0
- package/src/commands/helm/index.ts +1126 -0
- package/src/commands/help.ts +539 -0
- package/src/commands/history.ts +142 -0
- package/src/commands/import.ts +868 -0
- package/src/commands/index.ts +367 -0
- package/src/commands/init.ts +1046 -0
- package/src/commands/k8s/index.ts +1137 -0
- package/src/commands/login.ts +631 -0
- package/src/commands/logout.ts +83 -0
- package/src/commands/onboarding.ts +228 -0
- package/src/commands/plan/display.ts +279 -0
- package/src/commands/plan/index.ts +599 -0
- package/src/commands/preview.ts +452 -0
- package/src/commands/questionnaire.ts +1270 -0
- package/src/commands/resume.ts +55 -0
- package/src/commands/team/index.ts +346 -0
- package/src/commands/template.ts +232 -0
- package/src/commands/tf/index.ts +1034 -0
- package/src/commands/upgrade.ts +550 -0
- package/src/commands/usage/index.ts +134 -0
- package/src/commands/version.ts +170 -0
- package/src/compat/index.ts +2 -0
- package/src/compat/runtime.ts +12 -0
- package/src/compat/sqlite.ts +107 -0
- package/src/config/index.ts +17 -0
- package/src/config/manager.ts +530 -0
- package/src/config/safety-policy.ts +358 -0
- package/src/config/schema.ts +125 -0
- package/src/config/types.ts +527 -0
- package/src/context/context-db.ts +199 -0
- package/src/demo/index.ts +349 -0
- package/src/demo/scenarios/full-journey.ts +229 -0
- package/src/demo/scenarios/getting-started.ts +127 -0
- package/src/demo/scenarios/helm-release.ts +341 -0
- package/src/demo/scenarios/k8s-deployment.ts +194 -0
- package/src/demo/scenarios/terraform-vpc.ts +170 -0
- package/src/demo/types.ts +92 -0
- package/src/engine/cost-estimator.ts +438 -0
- package/src/engine/diagram-generator.ts +256 -0
- package/src/engine/drift-detector.ts +902 -0
- package/src/engine/executor.ts +1035 -0
- package/src/engine/index.ts +76 -0
- package/src/engine/orchestrator.ts +636 -0
- package/src/engine/planner.ts +720 -0
- package/src/engine/safety.ts +743 -0
- package/src/engine/verifier.ts +770 -0
- package/src/enterprise/audit.ts +348 -0
- package/src/enterprise/auth.ts +270 -0
- package/src/enterprise/billing.ts +822 -0
- package/src/enterprise/index.ts +17 -0
- package/src/enterprise/teams.ts +443 -0
- package/src/generator/best-practices.ts +1608 -0
- package/src/generator/helm.ts +630 -0
- package/src/generator/index.ts +37 -0
- package/src/generator/intent-parser.ts +514 -0
- package/src/generator/kubernetes.ts +976 -0
- package/src/generator/terraform.ts +1867 -0
- package/src/history/index.ts +8 -0
- package/src/history/manager.ts +322 -0
- package/src/history/types.ts +34 -0
- package/src/hooks/config.ts +432 -0
- package/src/hooks/engine.ts +391 -0
- package/src/hooks/index.ts +4 -0
- package/src/llm/auth-bridge.ts +198 -0
- package/src/llm/circuit-breaker.ts +140 -0
- package/src/llm/config-loader.ts +201 -0
- package/src/llm/cost-calculator.ts +171 -0
- package/src/llm/index.ts +8 -0
- package/src/llm/model-aliases.ts +115 -0
- package/src/llm/provider-registry.ts +63 -0
- package/src/llm/providers/anthropic.ts +433 -0
- package/src/llm/providers/bedrock.ts +477 -0
- package/src/llm/providers/google.ts +405 -0
- package/src/llm/providers/ollama.ts +767 -0
- package/src/llm/providers/openai-compatible.ts +340 -0
- package/src/llm/providers/openai.ts +328 -0
- package/src/llm/providers/openrouter.ts +338 -0
- package/src/llm/router.ts +1035 -0
- package/src/llm/types.ts +232 -0
- package/src/lsp/client.ts +298 -0
- package/src/lsp/languages.ts +116 -0
- package/src/lsp/manager.ts +278 -0
- package/src/mcp/client.ts +402 -0
- package/src/mcp/index.ts +5 -0
- package/src/mcp/manager.ts +133 -0
- package/src/nimbus.ts +214 -0
- package/src/plugins/index.ts +27 -0
- package/src/plugins/loader.ts +334 -0
- package/src/plugins/manager.ts +376 -0
- package/src/plugins/types.ts +284 -0
- package/src/scanners/cicd-scanner.ts +258 -0
- package/src/scanners/cloud-scanner.ts +466 -0
- package/src/scanners/framework-scanner.ts +469 -0
- package/src/scanners/iac-scanner.ts +388 -0
- package/src/scanners/index.ts +539 -0
- package/src/scanners/language-scanner.ts +276 -0
- package/src/scanners/package-manager-scanner.ts +277 -0
- package/src/scanners/types.ts +172 -0
- package/src/sessions/manager.ts +365 -0
- package/src/sessions/types.ts +44 -0
- package/src/sharing/sync.ts +296 -0
- package/src/sharing/viewer.ts +97 -0
- package/src/snapshots/index.ts +2 -0
- package/src/snapshots/manager.ts +530 -0
- package/src/state/artifacts.ts +147 -0
- package/src/state/audit.ts +137 -0
- package/src/state/billing.ts +240 -0
- package/src/state/checkpoints.ts +117 -0
- package/src/state/config.ts +67 -0
- package/src/state/conversations.ts +14 -0
- package/src/state/credentials.ts +154 -0
- package/src/state/db.ts +58 -0
- package/src/state/index.ts +26 -0
- package/src/state/messages.ts +115 -0
- package/src/state/projects.ts +123 -0
- package/src/state/schema.ts +236 -0
- package/src/state/sessions.ts +147 -0
- package/src/state/teams.ts +200 -0
- package/src/telemetry.ts +108 -0
- package/src/tools/aws-ops.ts +952 -0
- package/src/tools/azure-ops.ts +579 -0
- package/src/tools/file-ops.ts +593 -0
- package/src/tools/gcp-ops.ts +625 -0
- package/src/tools/git-ops.ts +773 -0
- package/src/tools/github-ops.ts +799 -0
- package/src/tools/helm-ops.ts +943 -0
- package/src/tools/index.ts +17 -0
- package/src/tools/k8s-ops.ts +819 -0
- package/src/tools/schemas/converter.ts +184 -0
- package/src/tools/schemas/devops.ts +612 -0
- package/src/tools/schemas/index.ts +73 -0
- package/src/tools/schemas/standard.ts +1144 -0
- package/src/tools/schemas/types.ts +705 -0
- package/src/tools/terraform-ops.ts +862 -0
- package/src/types/ambient.d.ts +193 -0
- package/src/types/config.ts +83 -0
- package/src/types/drift.ts +116 -0
- package/src/types/enterprise.ts +335 -0
- package/src/types/index.ts +20 -0
- package/src/types/plan.ts +44 -0
- package/src/types/request.ts +65 -0
- package/src/types/response.ts +54 -0
- package/src/types/service.ts +51 -0
- package/src/ui/App.tsx +997 -0
- package/src/ui/DeployPreview.tsx +169 -0
- package/src/ui/Header.tsx +68 -0
- package/src/ui/InputBox.tsx +350 -0
- package/src/ui/MessageList.tsx +585 -0
- package/src/ui/PermissionPrompt.tsx +151 -0
- package/src/ui/StatusBar.tsx +158 -0
- package/src/ui/ToolCallDisplay.tsx +409 -0
- package/src/ui/chat-ui.ts +853 -0
- package/src/ui/index.ts +33 -0
- package/src/ui/ink/index.ts +711 -0
- package/src/ui/streaming.ts +176 -0
- package/src/ui/types.ts +57 -0
- package/src/utils/analytics.ts +72 -0
- package/src/utils/cost-warning.ts +27 -0
- package/src/utils/env.ts +46 -0
- package/src/utils/errors.ts +69 -0
- package/src/utils/event-bus.ts +38 -0
- package/src/utils/index.ts +24 -0
- package/src/utils/logger.ts +171 -0
- package/src/utils/rate-limiter.ts +121 -0
- package/src/utils/service-auth.ts +49 -0
- package/src/utils/validation.ts +53 -0
- package/src/version.ts +4 -0
- package/src/watcher/index.ts +163 -0
- package/src/wizard/approval.ts +383 -0
- package/src/wizard/index.ts +25 -0
- package/src/wizard/prompts.ts +338 -0
- package/src/wizard/types.ts +171 -0
- package/src/wizard/ui.ts +556 -0
- package/src/wizard/wizard.ts +304 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Manager — Multi-Session Lifecycle
|
|
3
|
+
*
|
|
4
|
+
* Manages creation, switching, suspension, and destruction of parallel
|
|
5
|
+
* agent sessions. Each session has its own conversation history, tool
|
|
6
|
+
* state, and working context, but shares project config (NIMBUS.md).
|
|
7
|
+
*
|
|
8
|
+
* File conflict detection warns when two sessions edit the same file.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { getDb } from '../state/db';
|
|
12
|
+
import type { Database } from '../compat/sqlite';
|
|
13
|
+
import type { SessionRecord, SessionStatus, SessionEvent, SessionFileEdit } from './types';
|
|
14
|
+
import type { LLMMessage } from '../llm/types';
|
|
15
|
+
|
|
16
|
+
/** Singleton session manager instance. */
|
|
17
|
+
let instance: SessionManager | null = null;
|
|
18
|
+
|
|
19
|
+
export class SessionManager {
|
|
20
|
+
private db: Database;
|
|
21
|
+
private activeSessionId: string | null = null;
|
|
22
|
+
private fileEdits: Map<string, SessionFileEdit[]> = new Map();
|
|
23
|
+
private eventListeners: Array<(event: SessionEvent) => void> = [];
|
|
24
|
+
|
|
25
|
+
constructor(db?: Database) {
|
|
26
|
+
this.db = db || getDb();
|
|
27
|
+
this.ensureTable();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Get the singleton instance. */
|
|
31
|
+
static getInstance(db?: Database): SessionManager {
|
|
32
|
+
if (!instance) {
|
|
33
|
+
instance = new SessionManager(db);
|
|
34
|
+
}
|
|
35
|
+
return instance;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Reset the singleton (for testing). */
|
|
39
|
+
static resetInstance(): void {
|
|
40
|
+
instance = null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Ensure the sessions table exists with the status column. */
|
|
44
|
+
private ensureTable(): void {
|
|
45
|
+
this.db.exec(`
|
|
46
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
47
|
+
id TEXT PRIMARY KEY,
|
|
48
|
+
name TEXT NOT NULL,
|
|
49
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
50
|
+
mode TEXT NOT NULL DEFAULT 'plan',
|
|
51
|
+
model TEXT NOT NULL DEFAULT 'default',
|
|
52
|
+
cwd TEXT NOT NULL DEFAULT '.',
|
|
53
|
+
token_count INTEGER NOT NULL DEFAULT 0,
|
|
54
|
+
cost_usd REAL NOT NULL DEFAULT 0,
|
|
55
|
+
snapshot_count INTEGER NOT NULL DEFAULT 0,
|
|
56
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
57
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
58
|
+
metadata TEXT
|
|
59
|
+
)
|
|
60
|
+
`);
|
|
61
|
+
|
|
62
|
+
this.db.exec(`
|
|
63
|
+
CREATE TABLE IF NOT EXISTS conversations (
|
|
64
|
+
id TEXT PRIMARY KEY,
|
|
65
|
+
title TEXT NOT NULL DEFAULT 'Untitled',
|
|
66
|
+
messages TEXT NOT NULL DEFAULT '[]',
|
|
67
|
+
model TEXT NOT NULL DEFAULT 'default',
|
|
68
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
69
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
70
|
+
)
|
|
71
|
+
`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Create a new session. */
|
|
75
|
+
create(options: {
|
|
76
|
+
name: string;
|
|
77
|
+
mode?: SessionRecord['mode'];
|
|
78
|
+
model?: string;
|
|
79
|
+
cwd?: string;
|
|
80
|
+
}): SessionRecord {
|
|
81
|
+
const id = crypto.randomUUID();
|
|
82
|
+
const now = new Date().toISOString();
|
|
83
|
+
const mode = options.mode ?? 'plan';
|
|
84
|
+
const model = options.model ?? 'default';
|
|
85
|
+
const cwd = options.cwd ?? process.cwd();
|
|
86
|
+
|
|
87
|
+
this.db
|
|
88
|
+
.prepare(
|
|
89
|
+
`
|
|
90
|
+
INSERT INTO sessions (id, name, status, mode, model, cwd, token_count, cost_usd, snapshot_count, created_at, updated_at)
|
|
91
|
+
VALUES (?, ?, 'active', ?, ?, ?, 0, 0, 0, ?, ?)
|
|
92
|
+
`
|
|
93
|
+
)
|
|
94
|
+
.run(id, options.name, mode, model, cwd, now, now);
|
|
95
|
+
|
|
96
|
+
const session: SessionRecord = {
|
|
97
|
+
id,
|
|
98
|
+
name: options.name,
|
|
99
|
+
status: 'active',
|
|
100
|
+
mode,
|
|
101
|
+
model,
|
|
102
|
+
cwd,
|
|
103
|
+
tokenCount: 0,
|
|
104
|
+
costUSD: 0,
|
|
105
|
+
snapshotCount: 0,
|
|
106
|
+
createdAt: now,
|
|
107
|
+
updatedAt: now,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
this.emit({ type: 'created', sessionId: id, timestamp: new Date() });
|
|
111
|
+
return session;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** List all sessions, optionally filtered by status. */
|
|
115
|
+
list(status?: SessionStatus): SessionRecord[] {
|
|
116
|
+
let rows: any[];
|
|
117
|
+
if (status) {
|
|
118
|
+
rows = this.db
|
|
119
|
+
.prepare('SELECT * FROM sessions WHERE status = ? ORDER BY updated_at DESC')
|
|
120
|
+
.all(status) as any[];
|
|
121
|
+
} else {
|
|
122
|
+
rows = this.db.prepare('SELECT * FROM sessions ORDER BY updated_at DESC').all() as any[];
|
|
123
|
+
}
|
|
124
|
+
return rows.map(rowToSession);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** List only active sessions. */
|
|
128
|
+
listActive(): SessionRecord[] {
|
|
129
|
+
return this.list('active');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Get a session by ID. */
|
|
133
|
+
get(id: string): SessionRecord | null {
|
|
134
|
+
const row: any = this.db.prepare('SELECT * FROM sessions WHERE id = ?').get(id);
|
|
135
|
+
return row ? rowToSession(row) : null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Get the currently active session ID. */
|
|
139
|
+
getActiveSessionId(): string | null {
|
|
140
|
+
return this.activeSessionId;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Switch to a different session. Suspends the current one. */
|
|
144
|
+
switchTo(sessionId: string): SessionRecord | null {
|
|
145
|
+
const session = this.get(sessionId);
|
|
146
|
+
if (!session) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Suspend current session
|
|
151
|
+
if (this.activeSessionId && this.activeSessionId !== sessionId) {
|
|
152
|
+
this.updateStatus(this.activeSessionId, 'suspended');
|
|
153
|
+
this.emit({
|
|
154
|
+
type: 'suspended',
|
|
155
|
+
sessionId: this.activeSessionId,
|
|
156
|
+
timestamp: new Date(),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Activate target session
|
|
161
|
+
this.updateStatus(sessionId, 'active');
|
|
162
|
+
this.activeSessionId = sessionId;
|
|
163
|
+
|
|
164
|
+
this.emit({ type: 'switched', sessionId, timestamp: new Date() });
|
|
165
|
+
return this.get(sessionId);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Suspend a session (keeps state, stops processing). */
|
|
169
|
+
suspend(sessionId: string): void {
|
|
170
|
+
this.updateStatus(sessionId, 'suspended');
|
|
171
|
+
if (this.activeSessionId === sessionId) {
|
|
172
|
+
this.activeSessionId = null;
|
|
173
|
+
}
|
|
174
|
+
this.emit({ type: 'suspended', sessionId, timestamp: new Date() });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Resume a suspended session. */
|
|
178
|
+
resume(sessionId: string): SessionRecord | null {
|
|
179
|
+
const session = this.get(sessionId);
|
|
180
|
+
if (!session || session.status === 'completed') {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
this.updateStatus(sessionId, 'active');
|
|
185
|
+
this.activeSessionId = sessionId;
|
|
186
|
+
this.emit({ type: 'resumed', sessionId, timestamp: new Date() });
|
|
187
|
+
return this.get(sessionId);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Mark a session as completed. */
|
|
191
|
+
complete(sessionId: string): void {
|
|
192
|
+
this.updateStatus(sessionId, 'completed');
|
|
193
|
+
if (this.activeSessionId === sessionId) {
|
|
194
|
+
this.activeSessionId = null;
|
|
195
|
+
}
|
|
196
|
+
this.emit({ type: 'completed', sessionId, timestamp: new Date() });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Destroy a session (removes from DB). */
|
|
200
|
+
destroy(sessionId: string): void {
|
|
201
|
+
this.db.prepare('DELETE FROM sessions WHERE id = ?').run(sessionId);
|
|
202
|
+
if (this.activeSessionId === sessionId) {
|
|
203
|
+
this.activeSessionId = null;
|
|
204
|
+
}
|
|
205
|
+
// Clean up file edits for this session
|
|
206
|
+
for (const [path, edits] of this.fileEdits) {
|
|
207
|
+
const filtered = edits.filter(e => e.sessionId !== sessionId);
|
|
208
|
+
if (filtered.length === 0) {
|
|
209
|
+
this.fileEdits.delete(path);
|
|
210
|
+
} else {
|
|
211
|
+
this.fileEdits.set(path, filtered);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
this.emit({ type: 'destroyed', sessionId, timestamp: new Date() });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Update session metadata (tokens, cost, mode, etc.). */
|
|
218
|
+
updateSession(
|
|
219
|
+
sessionId: string,
|
|
220
|
+
updates: Partial<
|
|
221
|
+
Pick<SessionRecord, 'tokenCount' | 'costUSD' | 'snapshotCount' | 'mode' | 'model'>
|
|
222
|
+
>
|
|
223
|
+
): void {
|
|
224
|
+
const parts: string[] = [];
|
|
225
|
+
const values: any[] = [];
|
|
226
|
+
|
|
227
|
+
if (updates.tokenCount !== undefined) {
|
|
228
|
+
parts.push('token_count = ?');
|
|
229
|
+
values.push(updates.tokenCount);
|
|
230
|
+
}
|
|
231
|
+
if (updates.costUSD !== undefined) {
|
|
232
|
+
parts.push('cost_usd = ?');
|
|
233
|
+
values.push(updates.costUSD);
|
|
234
|
+
}
|
|
235
|
+
if (updates.snapshotCount !== undefined) {
|
|
236
|
+
parts.push('snapshot_count = ?');
|
|
237
|
+
values.push(updates.snapshotCount);
|
|
238
|
+
}
|
|
239
|
+
if (updates.mode !== undefined) {
|
|
240
|
+
parts.push('mode = ?');
|
|
241
|
+
values.push(updates.mode);
|
|
242
|
+
}
|
|
243
|
+
if (updates.model !== undefined) {
|
|
244
|
+
parts.push('model = ?');
|
|
245
|
+
values.push(updates.model);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (parts.length === 0) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
parts.push("updated_at = datetime('now')");
|
|
253
|
+
values.push(sessionId);
|
|
254
|
+
|
|
255
|
+
this.db.prepare(`UPDATE sessions SET ${parts.join(', ')} WHERE id = ?`).run(...values);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Save conversation messages for a session. */
|
|
259
|
+
saveConversation(sessionId: string, messages: LLMMessage[]): void {
|
|
260
|
+
const existing = this.db.prepare('SELECT id FROM conversations WHERE id = ?').get(sessionId);
|
|
261
|
+
const session = this.get(sessionId);
|
|
262
|
+
const title = session?.name ?? 'Untitled';
|
|
263
|
+
const messagesJson = JSON.stringify(messages);
|
|
264
|
+
|
|
265
|
+
if (existing) {
|
|
266
|
+
this.db
|
|
267
|
+
.prepare("UPDATE conversations SET messages = ?, updated_at = datetime('now') WHERE id = ?")
|
|
268
|
+
.run(messagesJson, sessionId);
|
|
269
|
+
} else {
|
|
270
|
+
this.db
|
|
271
|
+
.prepare(
|
|
272
|
+
"INSERT INTO conversations (id, title, messages, model, created_at, updated_at) VALUES (?, ?, ?, ?, datetime('now'), datetime('now'))"
|
|
273
|
+
)
|
|
274
|
+
.run(sessionId, title, messagesJson, session?.model ?? 'default');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Load conversation messages for a session. Returns empty array if not found. */
|
|
279
|
+
loadConversation(sessionId: string): LLMMessage[] {
|
|
280
|
+
const row: any = this.db
|
|
281
|
+
.prepare('SELECT messages FROM conversations WHERE id = ?')
|
|
282
|
+
.get(sessionId);
|
|
283
|
+
if (!row?.messages) {
|
|
284
|
+
return [];
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
return JSON.parse(row.messages);
|
|
288
|
+
} catch {
|
|
289
|
+
return [];
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Record a file edit for conflict detection. */
|
|
294
|
+
recordFileEdit(sessionId: string, filePath: string): string[] {
|
|
295
|
+
const normalizedPath = filePath;
|
|
296
|
+
const edit: SessionFileEdit = { sessionId, filePath: normalizedPath, timestamp: new Date() };
|
|
297
|
+
|
|
298
|
+
const existing = this.fileEdits.get(normalizedPath) || [];
|
|
299
|
+
existing.push(edit);
|
|
300
|
+
this.fileEdits.set(normalizedPath, existing);
|
|
301
|
+
|
|
302
|
+
// Check for conflicts (other sessions editing the same file)
|
|
303
|
+
const conflicts: string[] = [];
|
|
304
|
+
const otherEditors = existing.filter(
|
|
305
|
+
e => e.sessionId !== sessionId && e.timestamp.getTime() > Date.now() - 5 * 60 * 1000 // Within last 5 minutes
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
for (const editor of otherEditors) {
|
|
309
|
+
conflicts.push(editor.sessionId);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (conflicts.length > 0) {
|
|
313
|
+
this.emit({
|
|
314
|
+
type: 'file_conflict',
|
|
315
|
+
sessionId,
|
|
316
|
+
timestamp: new Date(),
|
|
317
|
+
details: `File "${filePath}" is also being edited by session(s): ${conflicts.join(', ')}`,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return conflicts;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Listen for session events. */
|
|
325
|
+
onEvent(listener: (event: SessionEvent) => void): () => void {
|
|
326
|
+
this.eventListeners.push(listener);
|
|
327
|
+
return () => {
|
|
328
|
+
this.eventListeners = this.eventListeners.filter(l => l !== listener);
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private emit(event: SessionEvent): void {
|
|
333
|
+
for (const listener of this.eventListeners) {
|
|
334
|
+
try {
|
|
335
|
+
listener(event);
|
|
336
|
+
} catch {
|
|
337
|
+
/* ignore */
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private updateStatus(sessionId: string, status: SessionStatus): void {
|
|
343
|
+
this.db
|
|
344
|
+
.prepare("UPDATE sessions SET status = ?, updated_at = datetime('now') WHERE id = ?")
|
|
345
|
+
.run(status, sessionId);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Convert a raw DB row to a SessionRecord. */
|
|
350
|
+
function rowToSession(row: any): SessionRecord {
|
|
351
|
+
return {
|
|
352
|
+
id: row.id,
|
|
353
|
+
name: row.name,
|
|
354
|
+
status: row.status as SessionStatus,
|
|
355
|
+
mode: row.mode,
|
|
356
|
+
model: row.model,
|
|
357
|
+
cwd: row.cwd,
|
|
358
|
+
tokenCount: row.token_count,
|
|
359
|
+
costUSD: row.cost_usd,
|
|
360
|
+
snapshotCount: row.snapshot_count,
|
|
361
|
+
createdAt: row.created_at,
|
|
362
|
+
updatedAt: row.updated_at,
|
|
363
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session types for multi-session support.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Status of a session. */
|
|
6
|
+
export type SessionStatus = 'active' | 'suspended' | 'completed';
|
|
7
|
+
|
|
8
|
+
/** Core session information stored in the database. */
|
|
9
|
+
export interface SessionRecord {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
status: SessionStatus;
|
|
13
|
+
mode: 'plan' | 'build' | 'deploy';
|
|
14
|
+
model: string;
|
|
15
|
+
cwd: string;
|
|
16
|
+
tokenCount: number;
|
|
17
|
+
costUSD: number;
|
|
18
|
+
snapshotCount: number;
|
|
19
|
+
createdAt: string;
|
|
20
|
+
updatedAt: string;
|
|
21
|
+
metadata?: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Event emitted by the session manager. */
|
|
25
|
+
export interface SessionEvent {
|
|
26
|
+
type:
|
|
27
|
+
| 'created'
|
|
28
|
+
| 'switched'
|
|
29
|
+
| 'suspended'
|
|
30
|
+
| 'resumed'
|
|
31
|
+
| 'completed'
|
|
32
|
+
| 'destroyed'
|
|
33
|
+
| 'file_conflict';
|
|
34
|
+
sessionId: string;
|
|
35
|
+
timestamp: Date;
|
|
36
|
+
details?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Info about a file being edited in a session (for conflict detection). */
|
|
40
|
+
export interface SessionFileEdit {
|
|
41
|
+
sessionId: string;
|
|
42
|
+
filePath: string;
|
|
43
|
+
timestamp: Date;
|
|
44
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Sharing — Generate share IDs and sync session data
|
|
3
|
+
*
|
|
4
|
+
* Provides local sharing via the nimbus serve API.
|
|
5
|
+
* Shares are persisted to the SQLite `shares` table so they survive restarts.
|
|
6
|
+
* For hosted sharing via Supabase, the astron-landing web UI handles
|
|
7
|
+
* the sync directly using its existing Supabase integration.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { getConversation as _getConversation } from '../state/conversations';
|
|
11
|
+
import { SessionManager as _SessionManager } from '../sessions/manager';
|
|
12
|
+
import type { LLMMessage } from '../llm/types';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Dependency overrides for testing. Set these before calling shareSession/etc.
|
|
16
|
+
* In production code these remain undefined and the real implementations are used.
|
|
17
|
+
*/
|
|
18
|
+
export const _deps = {
|
|
19
|
+
getConversation: undefined as ((id: string) => any) | undefined,
|
|
20
|
+
getSessionManager: undefined as (() => { get: (id: string) => any }) | undefined,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function getConversation(id: string) {
|
|
24
|
+
return (_deps.getConversation ?? _getConversation)(id);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getSessionManager() {
|
|
28
|
+
return _deps.getSessionManager ? _deps.getSessionManager() : _SessionManager.getInstance();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A shared session snapshot. */
|
|
32
|
+
export interface SharedSession {
|
|
33
|
+
/** Unique share ID. */
|
|
34
|
+
id: string;
|
|
35
|
+
/** Session ID this share was created from. */
|
|
36
|
+
sessionId: string;
|
|
37
|
+
/** Session name. */
|
|
38
|
+
name: string;
|
|
39
|
+
/** Conversation messages at time of sharing. */
|
|
40
|
+
messages: LLMMessage[];
|
|
41
|
+
/** Model used. */
|
|
42
|
+
model: string;
|
|
43
|
+
/** Session mode. */
|
|
44
|
+
mode: string;
|
|
45
|
+
/** Total cost. */
|
|
46
|
+
costUSD: number;
|
|
47
|
+
/** Total tokens used. */
|
|
48
|
+
tokenCount: number;
|
|
49
|
+
/** When the share was created. */
|
|
50
|
+
createdAt: string;
|
|
51
|
+
/** When the share expires (30-day TTL). */
|
|
52
|
+
expiresAt: string;
|
|
53
|
+
/** Whether this is a live-updating share. */
|
|
54
|
+
isLive: boolean;
|
|
55
|
+
/** Access token for write access (optional). */
|
|
56
|
+
writeToken?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Lazily import the DB to avoid circular dependency.
|
|
61
|
+
*/
|
|
62
|
+
function getDb() {
|
|
63
|
+
try {
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
65
|
+
const { getDb: _getDb } = require('../state/db');
|
|
66
|
+
return _getDb();
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Generate a short, URL-safe share ID.
|
|
74
|
+
*/
|
|
75
|
+
function generateShareId(): string {
|
|
76
|
+
const bytes = crypto.getRandomValues(new Uint8Array(9));
|
|
77
|
+
return btoa(String.fromCharCode(...bytes))
|
|
78
|
+
.replace(/\+/g, '-')
|
|
79
|
+
.replace(/\//g, '_')
|
|
80
|
+
.replace(/=/g, '');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Generate a secure write token for controlling write access.
|
|
85
|
+
*/
|
|
86
|
+
function generateWriteToken(): string {
|
|
87
|
+
return crypto.randomUUID();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Create a share for a session.
|
|
92
|
+
*/
|
|
93
|
+
export function shareSession(
|
|
94
|
+
sessionId: string,
|
|
95
|
+
options?: {
|
|
96
|
+
isLive?: boolean;
|
|
97
|
+
ttlDays?: number;
|
|
98
|
+
}
|
|
99
|
+
): SharedSession | null {
|
|
100
|
+
const sessionManager = getSessionManager();
|
|
101
|
+
const session = sessionManager.get(sessionId);
|
|
102
|
+
if (!session) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const conversation = getConversation(sessionId);
|
|
107
|
+
if (!conversation) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const ttlDays = options?.ttlDays ?? 30;
|
|
112
|
+
const now = new Date();
|
|
113
|
+
const expiresAt = new Date(now.getTime() + ttlDays * 24 * 60 * 60 * 1000);
|
|
114
|
+
|
|
115
|
+
const shared: SharedSession = {
|
|
116
|
+
id: generateShareId(),
|
|
117
|
+
sessionId,
|
|
118
|
+
name: session.name,
|
|
119
|
+
messages: conversation.messages,
|
|
120
|
+
model: session.model,
|
|
121
|
+
mode: session.mode,
|
|
122
|
+
costUSD: session.costUSD,
|
|
123
|
+
tokenCount: session.tokenCount,
|
|
124
|
+
createdAt: now.toISOString(),
|
|
125
|
+
expiresAt: expiresAt.toISOString(),
|
|
126
|
+
isLive: options?.isLive ?? false,
|
|
127
|
+
writeToken: generateWriteToken(),
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Persist to SQLite
|
|
131
|
+
const db = getDb();
|
|
132
|
+
if (db) {
|
|
133
|
+
try {
|
|
134
|
+
db.run(
|
|
135
|
+
`INSERT INTO shares (id, session_id, name, messages, model, mode, cost_usd, token_count, is_live, write_token, created_at, expires_at)
|
|
136
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
137
|
+
[
|
|
138
|
+
shared.id,
|
|
139
|
+
shared.sessionId,
|
|
140
|
+
shared.name,
|
|
141
|
+
JSON.stringify(shared.messages),
|
|
142
|
+
shared.model,
|
|
143
|
+
shared.mode,
|
|
144
|
+
shared.costUSD,
|
|
145
|
+
shared.tokenCount,
|
|
146
|
+
shared.isLive ? 1 : 0,
|
|
147
|
+
shared.writeToken,
|
|
148
|
+
shared.createdAt,
|
|
149
|
+
shared.expiresAt,
|
|
150
|
+
]
|
|
151
|
+
);
|
|
152
|
+
} catch {
|
|
153
|
+
// Non-critical — share is still returned but won't survive restart
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return shared;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Convert a raw SQLite row to a SharedSession object.
|
|
162
|
+
*/
|
|
163
|
+
function rowToSharedSession(row: any, includeWriteToken = false): SharedSession {
|
|
164
|
+
const result: SharedSession = {
|
|
165
|
+
id: row.id,
|
|
166
|
+
sessionId: row.session_id,
|
|
167
|
+
name: row.name,
|
|
168
|
+
messages: JSON.parse(row.messages),
|
|
169
|
+
model: row.model || '',
|
|
170
|
+
mode: row.mode || '',
|
|
171
|
+
costUSD: row.cost_usd || 0,
|
|
172
|
+
tokenCount: row.token_count || 0,
|
|
173
|
+
createdAt: row.created_at,
|
|
174
|
+
expiresAt: row.expires_at,
|
|
175
|
+
isLive: !!row.is_live,
|
|
176
|
+
};
|
|
177
|
+
if (includeWriteToken) {
|
|
178
|
+
result.writeToken = row.write_token;
|
|
179
|
+
}
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get a shared session by share ID.
|
|
185
|
+
* If the share is live, refresh the messages from the current session.
|
|
186
|
+
*/
|
|
187
|
+
export function getSharedSession(shareId: string): SharedSession | null {
|
|
188
|
+
const db = getDb();
|
|
189
|
+
if (!db) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const now = new Date().toISOString();
|
|
195
|
+
const row = db
|
|
196
|
+
.query(`SELECT * FROM shares WHERE id = ? AND expires_at > ?`)
|
|
197
|
+
.get(shareId, now) as any;
|
|
198
|
+
|
|
199
|
+
if (!row) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const shared = rowToSharedSession(row);
|
|
204
|
+
|
|
205
|
+
// Refresh messages for live shares
|
|
206
|
+
if (shared.isLive) {
|
|
207
|
+
const conversation = getConversation(shared.sessionId);
|
|
208
|
+
if (conversation) {
|
|
209
|
+
shared.messages = conversation.messages;
|
|
210
|
+
// Update stored messages
|
|
211
|
+
try {
|
|
212
|
+
db.run(`UPDATE shares SET messages = ? WHERE id = ?`, [
|
|
213
|
+
JSON.stringify(shared.messages),
|
|
214
|
+
shareId,
|
|
215
|
+
]);
|
|
216
|
+
} catch {
|
|
217
|
+
/* non-critical */
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return shared;
|
|
223
|
+
} catch {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* List all active shares.
|
|
230
|
+
*/
|
|
231
|
+
export function listShares(): SharedSession[] {
|
|
232
|
+
const db = getDb();
|
|
233
|
+
if (!db) {
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
// Clean up expired shares first
|
|
239
|
+
const now = new Date().toISOString();
|
|
240
|
+
db.run(`DELETE FROM shares WHERE expires_at <= ?`, [now]);
|
|
241
|
+
|
|
242
|
+
const rows = db
|
|
243
|
+
.query(
|
|
244
|
+
`SELECT id, session_id, name, messages, model, mode, cost_usd, token_count, is_live, created_at, expires_at
|
|
245
|
+
FROM shares ORDER BY created_at DESC`
|
|
246
|
+
)
|
|
247
|
+
.all() as any[];
|
|
248
|
+
|
|
249
|
+
return rows.map(row => rowToSharedSession(row));
|
|
250
|
+
} catch {
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Delete a share.
|
|
257
|
+
*/
|
|
258
|
+
export function deleteShare(shareId: string): boolean {
|
|
259
|
+
const db = getDb();
|
|
260
|
+
if (!db) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const result = db.run(`DELETE FROM shares WHERE id = ?`, [shareId]);
|
|
266
|
+
return result.changes > 0;
|
|
267
|
+
} catch {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Get the share URL for a shared session.
|
|
274
|
+
*/
|
|
275
|
+
export function getShareUrl(shareId: string, baseUrl?: string): string {
|
|
276
|
+
const base = baseUrl ?? 'http://localhost:6001';
|
|
277
|
+
return `${base}/nimbus/share/${shareId}`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Clean up expired shares.
|
|
282
|
+
*/
|
|
283
|
+
export function cleanupExpiredShares(): number {
|
|
284
|
+
const db = getDb();
|
|
285
|
+
if (!db) {
|
|
286
|
+
return 0;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
const now = new Date().toISOString();
|
|
291
|
+
const result = db.run(`DELETE FROM shares WHERE expires_at <= ?`, [now]);
|
|
292
|
+
return result.changes;
|
|
293
|
+
} catch {
|
|
294
|
+
return 0;
|
|
295
|
+
}
|
|
296
|
+
}
|