autopilot-ai 1.21.0 → 1.22.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/dist/index.js +79 -1373
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -18192,8 +18192,7 @@ var init_aster_workspaces = __esm({
|
|
|
18192
18192
|
// src/server/aster-store.ts
|
|
18193
18193
|
var aster_store_exports = {};
|
|
18194
18194
|
__export(aster_store_exports, {
|
|
18195
|
-
AsterStore: () => AsterStore
|
|
18196
|
-
defaultAsterDbPath: () => defaultAsterDbPath
|
|
18195
|
+
AsterStore: () => AsterStore
|
|
18197
18196
|
});
|
|
18198
18197
|
import Database4 from "better-sqlite3";
|
|
18199
18198
|
import { createHash as createHash5, randomBytes as randomBytes2, randomUUID as randomUUID6 } from "crypto";
|
|
@@ -18207,22 +18206,6 @@ function defaultAsterDbPath() {
|
|
|
18207
18206
|
function hashToken(token) {
|
|
18208
18207
|
return createHash5("sha256").update(token).digest("hex");
|
|
18209
18208
|
}
|
|
18210
|
-
function cellMappingFromRow(row) {
|
|
18211
|
-
return {
|
|
18212
|
-
conversationId: row.conversation_id,
|
|
18213
|
-
credentialId: row.credential_id,
|
|
18214
|
-
workspaceId: row.workspace_id,
|
|
18215
|
-
model: row.model,
|
|
18216
|
-
sessionId: row.session_id,
|
|
18217
|
-
cellName: row.cell_name,
|
|
18218
|
-
cellId: row.cell_id ?? null,
|
|
18219
|
-
status: row.status,
|
|
18220
|
-
createAttempted: row.create_attempted === 1,
|
|
18221
|
-
eventCursor: row.event_cursor,
|
|
18222
|
-
createdAt: row.created_at,
|
|
18223
|
-
updatedAt: row.updated_at
|
|
18224
|
-
};
|
|
18225
|
-
}
|
|
18226
18209
|
function rowToConversation(row) {
|
|
18227
18210
|
return {
|
|
18228
18211
|
id: row.id,
|
|
@@ -18340,38 +18323,6 @@ var init_aster_store = __esm({
|
|
|
18340
18323
|
created_at INTEGER NOT NULL,
|
|
18341
18324
|
PRIMARY KEY (conversation_id, client_turn_id)
|
|
18342
18325
|
);
|
|
18343
|
-
CREATE TABLE IF NOT EXISTS aster_cell_mappings (
|
|
18344
|
-
conversation_id TEXT PRIMARY KEY,
|
|
18345
|
-
credential_id TEXT NOT NULL,
|
|
18346
|
-
workspace_id TEXT NOT NULL,
|
|
18347
|
-
model TEXT NOT NULL,
|
|
18348
|
-
session_id TEXT NOT NULL UNIQUE,
|
|
18349
|
-
cell_name TEXT NOT NULL UNIQUE,
|
|
18350
|
-
cell_id TEXT,
|
|
18351
|
-
create_key_hash TEXT NOT NULL,
|
|
18352
|
-
create_fingerprint TEXT NOT NULL,
|
|
18353
|
-
status TEXT NOT NULL,
|
|
18354
|
-
create_attempted INTEGER NOT NULL DEFAULT 0,
|
|
18355
|
-
event_cursor INTEGER NOT NULL DEFAULT 0,
|
|
18356
|
-
created_at INTEGER NOT NULL,
|
|
18357
|
-
updated_at INTEGER NOT NULL,
|
|
18358
|
-
UNIQUE(credential_id, create_key_hash)
|
|
18359
|
-
);
|
|
18360
|
-
CREATE INDEX IF NOT EXISTS idx_aster_cells_lifecycle ON aster_cell_mappings(status, updated_at);
|
|
18361
|
-
CREATE TABLE IF NOT EXISTS aster_github_grants (
|
|
18362
|
-
conversation_id TEXT PRIMARY KEY,
|
|
18363
|
-
token TEXT NOT NULL UNIQUE,
|
|
18364
|
-
repository TEXT NOT NULL,
|
|
18365
|
-
created_at INTEGER NOT NULL,
|
|
18366
|
-
revoked_at INTEGER
|
|
18367
|
-
);
|
|
18368
|
-
CREATE TABLE IF NOT EXISTS aster_conversation_projects (
|
|
18369
|
-
conversation_id TEXT PRIMARY KEY,
|
|
18370
|
-
kind TEXT NOT NULL,
|
|
18371
|
-
repository TEXT,
|
|
18372
|
-
branch TEXT,
|
|
18373
|
-
created_at INTEGER NOT NULL
|
|
18374
|
-
);
|
|
18375
18326
|
`);
|
|
18376
18327
|
try {
|
|
18377
18328
|
chmodSync2(dbPath, 384);
|
|
@@ -18381,49 +18332,6 @@ var init_aster_store = __esm({
|
|
|
18381
18332
|
close() {
|
|
18382
18333
|
this.db.close();
|
|
18383
18334
|
}
|
|
18384
|
-
reserveCellConversation(input) {
|
|
18385
|
-
const keyHash = createHash5("sha256").update(input.idempotencyKey).digest("hex");
|
|
18386
|
-
const fingerprint = createHash5("sha256").update(JSON.stringify([input.workspaceId, input.model])).digest("hex");
|
|
18387
|
-
const reserve = this.db.transaction(() => {
|
|
18388
|
-
const existing = this.db.prepare("SELECT * FROM aster_cell_mappings WHERE credential_id = ? AND create_key_hash = ?").get(input.credentialId, keyHash);
|
|
18389
|
-
if (existing) {
|
|
18390
|
-
const mapping = cellMappingFromRow(existing);
|
|
18391
|
-
return mapping.workspaceId === input.workspaceId && mapping.model === input.model ? { kind: "replay", mapping } : { kind: "conflict" };
|
|
18392
|
-
}
|
|
18393
|
-
const now2 = Date.now();
|
|
18394
|
-
this.db.prepare(`INSERT INTO aster_cell_mappings
|
|
18395
|
-
(conversation_id, credential_id, workspace_id, model, session_id, cell_name, create_key_hash, create_fingerprint, status, created_at, updated_at)
|
|
18396
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'provisioning', ?, ?)`).run(input.conversationId, input.credentialId, input.workspaceId, input.model, input.sessionId, input.cellName, keyHash, fingerprint, now2, now2);
|
|
18397
|
-
return { kind: "created", mapping: this.getCellMapping(input.conversationId) };
|
|
18398
|
-
});
|
|
18399
|
-
return reserve.immediate();
|
|
18400
|
-
}
|
|
18401
|
-
getCellMapping(conversationId) {
|
|
18402
|
-
const row = this.db.prepare("SELECT * FROM aster_cell_mappings WHERE conversation_id = ?").get(conversationId);
|
|
18403
|
-
return row ? cellMappingFromRow(row) : void 0;
|
|
18404
|
-
}
|
|
18405
|
-
listCellMappings() {
|
|
18406
|
-
const rows = this.db.prepare("SELECT * FROM aster_cell_mappings ORDER BY created_at").all();
|
|
18407
|
-
return rows.map(cellMappingFromRow);
|
|
18408
|
-
}
|
|
18409
|
-
getCredentialCellMapping(conversationId, credentialId) {
|
|
18410
|
-
const row = this.db.prepare("SELECT * FROM aster_cell_mappings WHERE conversation_id = ? AND credential_id = ?").get(conversationId, credentialId);
|
|
18411
|
-
return row ? cellMappingFromRow(row) : void 0;
|
|
18412
|
-
}
|
|
18413
|
-
markCellCreateAttempted(conversationId) {
|
|
18414
|
-
this.db.prepare("UPDATE aster_cell_mappings SET create_attempted = 1, updated_at = ? WHERE conversation_id = ?").run(Date.now(), conversationId);
|
|
18415
|
-
}
|
|
18416
|
-
setCellId(conversationId, cellId) {
|
|
18417
|
-
this.db.prepare("UPDATE aster_cell_mappings SET cell_id = ?, updated_at = ? WHERE conversation_id = ?").run(cellId, Date.now(), conversationId);
|
|
18418
|
-
}
|
|
18419
|
-
setCellStatus(conversationId, status) {
|
|
18420
|
-
this.db.prepare("UPDATE aster_cell_mappings SET status = ?, updated_at = ? WHERE conversation_id = ?").run(status, Date.now(), conversationId);
|
|
18421
|
-
}
|
|
18422
|
-
advanceCellEventCursor(conversationId, cursor) {
|
|
18423
|
-
if (!Number.isSafeInteger(cursor) || cursor < 0) throw new Error("invalid Hotcell event cursor");
|
|
18424
|
-
const result2 = this.db.prepare("UPDATE aster_cell_mappings SET event_cursor = ?, updated_at = ? WHERE conversation_id = ? AND event_cursor < ?").run(cursor, Date.now(), conversationId, cursor);
|
|
18425
|
-
return result2.changes === 1;
|
|
18426
|
-
}
|
|
18427
18335
|
createCredential(input) {
|
|
18428
18336
|
const name = input.name.trim();
|
|
18429
18337
|
if (!name || name.length > 100) throw new Error("credential name must be 1 to 100 characters");
|
|
@@ -18539,57 +18447,6 @@ var init_aster_store = __esm({
|
|
|
18539
18447
|
});
|
|
18540
18448
|
return update.immediate();
|
|
18541
18449
|
}
|
|
18542
|
-
setConversationStatus(conversationId, status, data = {}) {
|
|
18543
|
-
const update = this.db.transaction(() => {
|
|
18544
|
-
const now2 = Date.now();
|
|
18545
|
-
const result2 = this.db.prepare("UPDATE aster_conversations SET status = ?, updated_at = ? WHERE id = ? AND active_run_id IS NULL").run(status, now2, conversationId);
|
|
18546
|
-
if (result2.changes !== 1) return false;
|
|
18547
|
-
this.insertEvent(conversationId, null, "status", { status, ...data }, now2);
|
|
18548
|
-
return true;
|
|
18549
|
-
});
|
|
18550
|
-
return update.immediate();
|
|
18551
|
-
}
|
|
18552
|
-
/** Records a conversation's project once; the first value wins so retries can't change it. */
|
|
18553
|
-
setConversationProject(conversationId, project) {
|
|
18554
|
-
this.db.prepare(`INSERT OR IGNORE INTO aster_conversation_projects (conversation_id, kind, repository, branch, created_at)
|
|
18555
|
-
VALUES (?, ?, ?, ?, ?)`).run(conversationId, project.kind, project.repository, project.branch, Date.now());
|
|
18556
|
-
}
|
|
18557
|
-
getConversationProject(conversationId) {
|
|
18558
|
-
const row = this.db.prepare("SELECT kind, repository, branch FROM aster_conversation_projects WHERE conversation_id = ?").get(conversationId);
|
|
18559
|
-
if (!row) return void 0;
|
|
18560
|
-
return { kind: row.kind === "code" ? "code" : "chat", repository: row.repository, branch: row.branch };
|
|
18561
|
-
}
|
|
18562
|
-
/**
|
|
18563
|
-
* The repository-scoped GitHub token for a code conversation, created once. It is stored
|
|
18564
|
-
* as-is (like the server's own GitHub token, it never leaves the server except into that
|
|
18565
|
-
* conversation's cell) so provisioning retries hand the cell the same value.
|
|
18566
|
-
*/
|
|
18567
|
-
getOrCreateGitHubGrant(conversationId, repository) {
|
|
18568
|
-
const existing = this.db.prepare("SELECT token FROM aster_github_grants WHERE conversation_id = ? AND revoked_at IS NULL").get(conversationId);
|
|
18569
|
-
if (existing) return existing.token;
|
|
18570
|
-
const token = "aghp_" + randomBytes2(32).toString("base64url");
|
|
18571
|
-
this.db.prepare(`INSERT INTO aster_github_grants (conversation_id, token, repository, created_at) VALUES (?, ?, ?, ?)
|
|
18572
|
-
ON CONFLICT(conversation_id) DO UPDATE SET token = excluded.token, repository = excluded.repository, created_at = excluded.created_at, revoked_at = NULL`).run(conversationId, token, repository, Date.now());
|
|
18573
|
-
return token;
|
|
18574
|
-
}
|
|
18575
|
-
resolveGitHubGrant(token) {
|
|
18576
|
-
const row = this.db.prepare("SELECT conversation_id, repository FROM aster_github_grants WHERE token = ? AND revoked_at IS NULL").get(token);
|
|
18577
|
-
return row ? { conversationId: row.conversation_id, repository: row.repository } : void 0;
|
|
18578
|
-
}
|
|
18579
|
-
revokeGitHubGrant(conversationId) {
|
|
18580
|
-
this.db.prepare("UPDATE aster_github_grants SET revoked_at = ? WHERE conversation_id = ? AND revoked_at IS NULL").run(Date.now(), conversationId);
|
|
18581
|
-
}
|
|
18582
|
-
/** Switch an idle conversation's model; refused while a turn is active. */
|
|
18583
|
-
setConversationModel(conversationId, model) {
|
|
18584
|
-
const update = this.db.transaction(() => {
|
|
18585
|
-
const now2 = Date.now();
|
|
18586
|
-
const result2 = this.db.prepare("UPDATE aster_conversations SET model = ?, updated_at = ? WHERE id = ? AND active_run_id IS NULL").run(model, now2, conversationId);
|
|
18587
|
-
if (result2.changes !== 1) return false;
|
|
18588
|
-
this.insertEvent(conversationId, null, "model.changed", { model }, now2);
|
|
18589
|
-
return true;
|
|
18590
|
-
});
|
|
18591
|
-
return update.immediate();
|
|
18592
|
-
}
|
|
18593
18450
|
appendEvent(conversationId, runId, type, data) {
|
|
18594
18451
|
const append = this.db.transaction(() => this.insertEvent(conversationId, runId, type, data, Date.now()));
|
|
18595
18452
|
return append.immediate();
|
|
@@ -19492,103 +19349,6 @@ var init_wake_scheduler = __esm({
|
|
|
19492
19349
|
}
|
|
19493
19350
|
});
|
|
19494
19351
|
|
|
19495
|
-
// src/server/aster-github.ts
|
|
19496
|
-
function mapRepository(raw) {
|
|
19497
|
-
return {
|
|
19498
|
-
fullName: raw.full_name,
|
|
19499
|
-
name: raw.name,
|
|
19500
|
-
owner: raw.owner?.login ?? raw.full_name.split("/")[0] ?? "",
|
|
19501
|
-
private: raw.private,
|
|
19502
|
-
description: raw.description,
|
|
19503
|
-
defaultBranch: raw.default_branch ?? "main",
|
|
19504
|
-
updatedAt: raw.pushed_at ?? raw.updated_at ?? null
|
|
19505
|
-
};
|
|
19506
|
-
}
|
|
19507
|
-
var AsterGitHubError, REPOSITORY_FULL_NAME_RE, REPOSITORY_NAME_RE, API, AsterGitHub;
|
|
19508
|
-
var init_aster_github = __esm({
|
|
19509
|
-
"src/server/aster-github.ts"() {
|
|
19510
|
-
"use strict";
|
|
19511
|
-
AsterGitHubError = class extends Error {
|
|
19512
|
-
constructor(message2, status, code) {
|
|
19513
|
-
super(message2);
|
|
19514
|
-
this.status = status;
|
|
19515
|
-
this.code = code;
|
|
19516
|
-
this.name = "AsterGitHubError";
|
|
19517
|
-
}
|
|
19518
|
-
status;
|
|
19519
|
-
code;
|
|
19520
|
-
};
|
|
19521
|
-
REPOSITORY_FULL_NAME_RE = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/[A-Za-z0-9._-]{1,100}$/;
|
|
19522
|
-
REPOSITORY_NAME_RE = /^[A-Za-z0-9._-]{1,100}$/;
|
|
19523
|
-
API = "https://api.github.com";
|
|
19524
|
-
AsterGitHub = class {
|
|
19525
|
-
constructor(token = process.env.GITHUB_TOKEN, fetchImpl2 = fetch) {
|
|
19526
|
-
this.token = token;
|
|
19527
|
-
this.fetchImpl = fetchImpl2;
|
|
19528
|
-
}
|
|
19529
|
-
token;
|
|
19530
|
-
fetchImpl;
|
|
19531
|
-
get configured() {
|
|
19532
|
-
return Boolean(this.token?.trim());
|
|
19533
|
-
}
|
|
19534
|
-
/** Repositories the token can push to, most recently pushed first. */
|
|
19535
|
-
async listRepositories(query = "") {
|
|
19536
|
-
const raw = await this.request(
|
|
19537
|
-
"GET",
|
|
19538
|
-
"/user/repos?per_page=100&sort=pushed&affiliation=owner,collaborator,organization_member"
|
|
19539
|
-
);
|
|
19540
|
-
const needle = query.trim().toLowerCase();
|
|
19541
|
-
return raw.map(mapRepository).filter((repo) => !needle || repo.fullName.toLowerCase().includes(needle) || (repo.description ?? "").toLowerCase().includes(needle));
|
|
19542
|
-
}
|
|
19543
|
-
async getRepository(fullName) {
|
|
19544
|
-
if (!REPOSITORY_FULL_NAME_RE.test(fullName)) throw new AsterGitHubError("Invalid repository name", 400, "invalid_repository");
|
|
19545
|
-
return mapRepository(await this.request("GET", `/repos/${fullName}`));
|
|
19546
|
-
}
|
|
19547
|
-
/** Creates a repository under the token's user, initialized so it has a default branch to clone. */
|
|
19548
|
-
async createRepository(input) {
|
|
19549
|
-
if (!REPOSITORY_NAME_RE.test(input.name) || input.name === "." || input.name === "..") {
|
|
19550
|
-
throw new AsterGitHubError("Repository names may use letters, digits, '.', '_' and '-'", 400, "invalid_repository_name");
|
|
19551
|
-
}
|
|
19552
|
-
const description = input.description?.trim().slice(0, 350);
|
|
19553
|
-
return mapRepository(await this.request("POST", "/user/repos", {
|
|
19554
|
-
name: input.name,
|
|
19555
|
-
private: input.private,
|
|
19556
|
-
auto_init: true,
|
|
19557
|
-
...description ? { description } : {}
|
|
19558
|
-
}));
|
|
19559
|
-
}
|
|
19560
|
-
async request(method, path, body) {
|
|
19561
|
-
if (!this.configured) throw new AsterGitHubError("GitHub is not connected on the server", 503, "github_not_configured");
|
|
19562
|
-
let response;
|
|
19563
|
-
try {
|
|
19564
|
-
response = await this.fetchImpl(API + path, {
|
|
19565
|
-
method,
|
|
19566
|
-
headers: {
|
|
19567
|
-
Accept: "application/vnd.github+json",
|
|
19568
|
-
Authorization: `Bearer ${this.token}`,
|
|
19569
|
-
"X-GitHub-Api-Version": "2022-11-28",
|
|
19570
|
-
"User-Agent": "autopilot-aster",
|
|
19571
|
-
...body === void 0 ? {} : { "Content-Type": "application/json" }
|
|
19572
|
-
},
|
|
19573
|
-
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
19574
|
-
signal: AbortSignal.timeout(2e4)
|
|
19575
|
-
});
|
|
19576
|
-
} catch {
|
|
19577
|
-
throw new AsterGitHubError("GitHub could not be reached", 502, "github_unreachable");
|
|
19578
|
-
}
|
|
19579
|
-
if (response.ok) return await response.json();
|
|
19580
|
-
const detail = await response.json().catch(() => ({}));
|
|
19581
|
-
const message2 = detail.errors?.[0]?.message ?? detail.message ?? `GitHub returned HTTP ${response.status}`;
|
|
19582
|
-
if (response.status === 404) throw new AsterGitHubError("Repository not found or not accessible with the server's GitHub token", 404, "repository_not_found");
|
|
19583
|
-
if (response.status === 401) throw new AsterGitHubError("The server's GitHub token was rejected", 502, "github_token_rejected");
|
|
19584
|
-
if (response.status === 403) throw new AsterGitHubError(`GitHub refused the request: ${message2}`, 403, "github_forbidden");
|
|
19585
|
-
if (response.status === 422) throw new AsterGitHubError(message2, 422, "github_validation_failed");
|
|
19586
|
-
throw new AsterGitHubError(message2, 502, "github_error");
|
|
19587
|
-
}
|
|
19588
|
-
};
|
|
19589
|
-
}
|
|
19590
|
-
});
|
|
19591
|
-
|
|
19592
19352
|
// src/server/aster-tools.ts
|
|
19593
19353
|
import { realpath as realpath5 } from "fs/promises";
|
|
19594
19354
|
import { isAbsolute as isAbsolute6, relative as relative9, sep as sep5 } from "path";
|
|
@@ -19629,31 +19389,6 @@ async function assertAsterWorkspacePath(workspaceRoot, value, allowMissing = fal
|
|
|
19629
19389
|
assertNotSensitive(root, actual);
|
|
19630
19390
|
return candidate;
|
|
19631
19391
|
}
|
|
19632
|
-
function assertAsterCellPath(value) {
|
|
19633
|
-
if (typeof value !== "string" || !value.trim()) throw new Error("workspace_path_invalid");
|
|
19634
|
-
const root = "/workspace";
|
|
19635
|
-
const candidate = value.startsWith("/") ? posixNormalize(value) : posixNormalize(`${root}/${value}`);
|
|
19636
|
-
if (candidate !== root && !candidate.startsWith(root + "/")) throw new Error("workspace_path_forbidden");
|
|
19637
|
-
const parts = candidate.slice(root.length).split("/").filter(Boolean).map((part) => part.toLowerCase());
|
|
19638
|
-
if (parts.some((part) => part === ".env" || part.startsWith(".env.") || SENSITIVE_NAMES.has(part))) {
|
|
19639
|
-
throw new Error("workspace_sensitive_path_forbidden");
|
|
19640
|
-
}
|
|
19641
|
-
return candidate;
|
|
19642
|
-
}
|
|
19643
|
-
function posixNormalize(value) {
|
|
19644
|
-
const parts = value.split("/");
|
|
19645
|
-
const out = [];
|
|
19646
|
-
for (const part of parts) {
|
|
19647
|
-
if (!part || part === ".") continue;
|
|
19648
|
-
if (part === "..") {
|
|
19649
|
-
if (out.length === 0) return "/__escape__";
|
|
19650
|
-
out.pop();
|
|
19651
|
-
continue;
|
|
19652
|
-
}
|
|
19653
|
-
out.push(part);
|
|
19654
|
-
}
|
|
19655
|
-
return "/" + out.join("/");
|
|
19656
|
-
}
|
|
19657
19392
|
function isLikelySkKey(candidate) {
|
|
19658
19393
|
if (/^sk-or-v1-[A-Za-z0-9]{16,}/.test(candidate)) return true;
|
|
19659
19394
|
return candidate.slice(3).split(/[-_]/).some((chunk) => chunk.length >= 20 && /\d/.test(chunk));
|
|
@@ -19704,517 +19439,8 @@ var init_aster_tools = __esm({
|
|
|
19704
19439
|
}
|
|
19705
19440
|
});
|
|
19706
19441
|
|
|
19707
|
-
// src/server/aster-memory.ts
|
|
19708
|
-
import Database5 from "better-sqlite3";
|
|
19709
|
-
import { randomUUID as randomUUID7 } from "crypto";
|
|
19710
|
-
import { chmodSync as chmodSync3, mkdirSync as mkdirSync10 } from "fs";
|
|
19711
|
-
import { dirname as dirname20 } from "path";
|
|
19712
|
-
function rowToItem(row) {
|
|
19713
|
-
return {
|
|
19714
|
-
id: row.id,
|
|
19715
|
-
kind: row.kind,
|
|
19716
|
-
key: row.key,
|
|
19717
|
-
title: row.title,
|
|
19718
|
-
content: row.content,
|
|
19719
|
-
pinned: row.pinned === 1,
|
|
19720
|
-
sourceConversationId: row.source_conversation_id,
|
|
19721
|
-
createdAt: row.created_at,
|
|
19722
|
-
updatedAt: row.updated_at
|
|
19723
|
-
};
|
|
19724
|
-
}
|
|
19725
|
-
function normalizeKey(kind, value) {
|
|
19726
|
-
const raw = value.toLowerCase().startsWith(`${kind}:`) ? value.slice(kind.length + 1) : value;
|
|
19727
|
-
const slug = raw.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
19728
|
-
const key = `${kind}:${slug}`;
|
|
19729
|
-
return KEY_RE.test(key) ? key : null;
|
|
19730
|
-
}
|
|
19731
|
-
function formatCard(card) {
|
|
19732
|
-
return `- ${card.title} (${card.kind}): ${card.content}`;
|
|
19733
|
-
}
|
|
19734
|
-
function mentions(text, card) {
|
|
19735
|
-
const haystack = ` ${text.toLowerCase()} `;
|
|
19736
|
-
const words = /* @__PURE__ */ new Set();
|
|
19737
|
-
for (const part of [card.title, card.key?.split(":")[1]?.replace(/-/g, " ") ?? ""]) {
|
|
19738
|
-
for (const word of part.toLowerCase().split(/[^\p{L}\p{N}]+/u)) {
|
|
19739
|
-
if (word.length >= 3 && !STOP_WORDS.has(word)) words.add(word);
|
|
19740
|
-
}
|
|
19741
|
-
}
|
|
19742
|
-
return [...words].some((word) => new RegExp(`[^\\p{L}\\p{N}]${escapeRegExp(word)}[^\\p{L}\\p{N}]`, "u").test(haystack));
|
|
19743
|
-
}
|
|
19744
|
-
function escapeRegExp(value) {
|
|
19745
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
19746
|
-
}
|
|
19747
|
-
var MAX_CARD_CHARS, MAX_FACT_CHARS, MAX_MEMORY_PROMPT_ITEMS, BRIEF_FACTS, BRIEF_RECENT_SESSIONS, BRIEF_RECENT_CARDS, KEY_RE, AsterMemory, EXTRACTION_PROMPT, STOP_WORDS;
|
|
19748
|
-
var init_aster_memory = __esm({
|
|
19749
|
-
"src/server/aster-memory.ts"() {
|
|
19750
|
-
"use strict";
|
|
19751
|
-
init_openrouter();
|
|
19752
|
-
init_aster_store();
|
|
19753
|
-
init_aster_tools();
|
|
19754
|
-
MAX_CARD_CHARS = 900;
|
|
19755
|
-
MAX_FACT_CHARS = 300;
|
|
19756
|
-
MAX_MEMORY_PROMPT_ITEMS = 150;
|
|
19757
|
-
BRIEF_FACTS = 15;
|
|
19758
|
-
BRIEF_RECENT_SESSIONS = 6;
|
|
19759
|
-
BRIEF_RECENT_CARDS = 3;
|
|
19760
|
-
KEY_RE = /^(person|project):[a-z0-9][a-z0-9-]{0,80}$/;
|
|
19761
|
-
AsterMemory = class {
|
|
19762
|
-
constructor(dbPath = defaultAsterDbPath(), options = {}) {
|
|
19763
|
-
this.options = options;
|
|
19764
|
-
mkdirSync10(dirname20(dbPath), { recursive: true, mode: 448 });
|
|
19765
|
-
this.db = new Database5(dbPath);
|
|
19766
|
-
this.db.pragma("journal_mode = WAL");
|
|
19767
|
-
this.db.pragma("busy_timeout = 5000");
|
|
19768
|
-
this.db.exec(`
|
|
19769
|
-
CREATE TABLE IF NOT EXISTS aster_memories (
|
|
19770
|
-
id TEXT PRIMARY KEY,
|
|
19771
|
-
kind TEXT NOT NULL,
|
|
19772
|
-
key TEXT,
|
|
19773
|
-
title TEXT NOT NULL,
|
|
19774
|
-
content TEXT NOT NULL,
|
|
19775
|
-
pinned INTEGER NOT NULL DEFAULT 0,
|
|
19776
|
-
source_conversation_id TEXT,
|
|
19777
|
-
created_at INTEGER NOT NULL,
|
|
19778
|
-
updated_at INTEGER NOT NULL,
|
|
19779
|
-
deleted INTEGER NOT NULL DEFAULT 0
|
|
19780
|
-
);
|
|
19781
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_aster_memories_key ON aster_memories(key) WHERE key IS NOT NULL AND deleted = 0;
|
|
19782
|
-
CREATE INDEX IF NOT EXISTS idx_aster_memories_kind ON aster_memories(kind, deleted, updated_at);
|
|
19783
|
-
`);
|
|
19784
|
-
try {
|
|
19785
|
-
chmodSync3(dbPath, 384);
|
|
19786
|
-
} catch {
|
|
19787
|
-
}
|
|
19788
|
-
}
|
|
19789
|
-
options;
|
|
19790
|
-
db;
|
|
19791
|
-
/** Card keys already shown to the agent per conversation, so later turns only add new ones. */
|
|
19792
|
-
shown = /* @__PURE__ */ new Map();
|
|
19793
|
-
get enabled() {
|
|
19794
|
-
return this.options.enabled !== false && Boolean(this.apiKey);
|
|
19795
|
-
}
|
|
19796
|
-
get apiKey() {
|
|
19797
|
-
return this.options.apiKey ?? process.env.OPENROUTER_API_KEY;
|
|
19798
|
-
}
|
|
19799
|
-
get model() {
|
|
19800
|
-
return this.options.model ?? process.env.AUTOPILOT_ASTER_MEMORY_MODEL ?? "google/gemini-3.8-flash";
|
|
19801
|
-
}
|
|
19802
|
-
close() {
|
|
19803
|
-
this.db.close();
|
|
19804
|
-
}
|
|
19805
|
-
list(kind) {
|
|
19806
|
-
const rows = kind ? this.db.prepare("SELECT * FROM aster_memories WHERE deleted = 0 AND kind = ? ORDER BY updated_at DESC").all(kind) : this.db.prepare("SELECT * FROM aster_memories WHERE deleted = 0 ORDER BY updated_at DESC").all();
|
|
19807
|
-
return rows.map(rowToItem);
|
|
19808
|
-
}
|
|
19809
|
-
get(id) {
|
|
19810
|
-
const row = this.db.prepare("SELECT * FROM aster_memories WHERE id = ? AND deleted = 0").get(id);
|
|
19811
|
-
return row ? rowToItem(row) : void 0;
|
|
19812
|
-
}
|
|
19813
|
-
/** Soft delete; the user removed it, so extraction must not silently recreate it from this item. */
|
|
19814
|
-
delete(id) {
|
|
19815
|
-
return this.db.prepare("UPDATE aster_memories SET deleted = 1, updated_at = ? WHERE id = ? AND deleted = 0").run(Date.now(), id).changes === 1;
|
|
19816
|
-
}
|
|
19817
|
-
/** Write or merge one item. person/project/session are keyed; facts are by id. */
|
|
19818
|
-
upsert(input) {
|
|
19819
|
-
const now2 = Date.now();
|
|
19820
|
-
const limit = input.kind === "fact" ? MAX_FACT_CHARS : MAX_CARD_CHARS;
|
|
19821
|
-
const title = input.title.trim().slice(0, 120);
|
|
19822
|
-
const content = input.content.trim().slice(0, limit);
|
|
19823
|
-
const existing = input.key ? this.db.prepare("SELECT * FROM aster_memories WHERE key = ? AND deleted = 0").get(input.key) : input.id ? this.db.prepare("SELECT * FROM aster_memories WHERE id = ? AND deleted = 0").get(input.id) : void 0;
|
|
19824
|
-
if (existing) {
|
|
19825
|
-
this.db.prepare("UPDATE aster_memories SET title = ?, content = ?, pinned = ?, updated_at = ? WHERE id = ?").run(title, content, existing.pinned || input.pinned ? 1 : 0, now2, existing.id);
|
|
19826
|
-
return { item: this.get(existing.id), change: "updated" };
|
|
19827
|
-
}
|
|
19828
|
-
const id = randomUUID7();
|
|
19829
|
-
this.db.prepare(`INSERT INTO aster_memories (id, kind, key, title, content, pinned, source_conversation_id, created_at, updated_at)
|
|
19830
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, input.kind, input.key ?? null, title, content, input.pinned ? 1 : 0, input.sourceConversationId ?? null, now2, now2);
|
|
19831
|
-
return { item: this.get(id), change: "added" };
|
|
19832
|
-
}
|
|
19833
|
-
/**
|
|
19834
|
-
* The memory note for a turn: a full brief on a conversation's first turn, afterwards only
|
|
19835
|
-
* person/project cards the user's message newly brings up. Empty when there is nothing new.
|
|
19836
|
-
*/
|
|
19837
|
-
brief(conversationId, userText) {
|
|
19838
|
-
const items = this.list();
|
|
19839
|
-
if (items.length === 0) return "";
|
|
19840
|
-
const shown = this.shown.get(conversationId);
|
|
19841
|
-
const cards = items.filter((item) => item.kind === "person" || item.kind === "project");
|
|
19842
|
-
const mentioned = cards.filter((card) => mentions(userText, card));
|
|
19843
|
-
if (shown) {
|
|
19844
|
-
const fresh = mentioned.filter((card) => !shown.has(card.id));
|
|
19845
|
-
if (fresh.length === 0) return "";
|
|
19846
|
-
fresh.forEach((card) => shown.add(card.id));
|
|
19847
|
-
return `[Aster memory \u2014 relevant to this message]
|
|
19848
|
-
${fresh.map(formatCard).join("\n")}`;
|
|
19849
|
-
}
|
|
19850
|
-
const shownNow = /* @__PURE__ */ new Set();
|
|
19851
|
-
const facts = items.filter((item) => item.kind === "fact").sort((a, b) => Number(b.pinned) - Number(a.pinned) || b.updatedAt - a.updatedAt).slice(0, BRIEF_FACTS);
|
|
19852
|
-
const featured = [...mentioned, ...cards.filter((card) => !mentioned.includes(card)).slice(0, BRIEF_RECENT_CARDS)];
|
|
19853
|
-
featured.forEach((card) => shownNow.add(card.id));
|
|
19854
|
-
const others = cards.filter((card) => !shownNow.has(card.id));
|
|
19855
|
-
const sessions = items.filter((item) => item.kind === "session" && item.key !== conversationId).slice(0, BRIEF_RECENT_SESSIONS);
|
|
19856
|
-
this.shown.set(conversationId, shownNow);
|
|
19857
|
-
const sections = [
|
|
19858
|
-
"[Aster memory \u2014 what you know about the user from earlier chats. Use it naturally and don't recite it. The full memory is in /workspace/.aster/memory.md; grep it when the user mentions someone or something you don't recognize.]"
|
|
19859
|
-
];
|
|
19860
|
-
if (facts.length) sections.push("About the user:\n" + facts.map((fact) => `- ${fact.content}`).join("\n"));
|
|
19861
|
-
if (featured.length) sections.push("People and projects:\n" + featured.map(formatCard).join("\n"));
|
|
19862
|
-
if (others.length) sections.push("Also known (details in memory.md): " + others.map((card) => card.title).join(", "));
|
|
19863
|
-
if (sessions.length) sections.push("Recent chats:\n" + sessions.map((session) => `- ${session.title}: ${session.content}`).join("\n"));
|
|
19864
|
-
return sections.join("\n\n");
|
|
19865
|
-
}
|
|
19866
|
-
/** All memory as Markdown, written into the cell so the agent can search it on demand. */
|
|
19867
|
-
snapshot() {
|
|
19868
|
-
const items = this.list();
|
|
19869
|
-
const group = (kind, heading, render2) => {
|
|
19870
|
-
const entries = items.filter((item) => item.kind === kind);
|
|
19871
|
-
return entries.length ? `## ${heading}
|
|
19872
|
-
|
|
19873
|
-
${entries.map(render2).join("\n")}
|
|
19874
|
-
` : "";
|
|
19875
|
-
};
|
|
19876
|
-
return [
|
|
19877
|
-
"# Aster memory\n\nWhat the user's assistant remembers across chats. Read-only here; it is updated automatically after each turn.\n",
|
|
19878
|
-
group("fact", "About the user", (item) => `- ${item.content}`),
|
|
19879
|
-
group("person", "People", (item) => `### ${item.title}
|
|
19880
|
-
${item.content}
|
|
19881
|
-
`),
|
|
19882
|
-
group("project", "Projects", (item) => `### ${item.title}
|
|
19883
|
-
${item.content}
|
|
19884
|
-
`),
|
|
19885
|
-
group("session", "Chats", (item) => `- **${item.title}** (${new Date(item.updatedAt).toISOString().slice(0, 10)}): ${item.content}`)
|
|
19886
|
-
].filter(Boolean).join("\n");
|
|
19887
|
-
}
|
|
19888
|
-
/** Read one exchange and update memory. Returns what changed (for the app's "memory updated" chip). */
|
|
19889
|
-
async remember(exchange) {
|
|
19890
|
-
if (!this.enabled) return [];
|
|
19891
|
-
const result2 = await this.extract(exchange);
|
|
19892
|
-
if (!result2) return [];
|
|
19893
|
-
const changes = [];
|
|
19894
|
-
const known = new Map(this.list().filter((item) => item.kind !== "session").map((item) => [item.id, item]));
|
|
19895
|
-
for (const id of result2.deletes ?? []) {
|
|
19896
|
-
const item = known.get(id);
|
|
19897
|
-
if (item && !item.pinned && this.delete(id)) changes.push({ id, kind: item.kind, title: item.title, change: "removed" });
|
|
19898
|
-
}
|
|
19899
|
-
for (const upsert of result2.upserts ?? []) {
|
|
19900
|
-
const kind = upsert.kind === "person" || upsert.kind === "project" || upsert.kind === "fact" ? upsert.kind : void 0;
|
|
19901
|
-
const title = upsert.title?.trim();
|
|
19902
|
-
const content = upsert.content?.trim();
|
|
19903
|
-
if (!kind || !title || !content || containsLikelyProviderSecret(content)) continue;
|
|
19904
|
-
const key = kind === "fact" ? null : normalizeKey(kind, upsert.key ?? title);
|
|
19905
|
-
if (kind !== "fact" && !key) continue;
|
|
19906
|
-
const id = kind === "fact" && upsert.id && known.has(upsert.id) ? upsert.id : null;
|
|
19907
|
-
const { item, change } = this.upsert({ kind, key, id, title, content, pinned: upsert.pinned === true, sourceConversationId: exchange.conversationId });
|
|
19908
|
-
changes.push({ id: item.id, kind, title: item.title, change });
|
|
19909
|
-
}
|
|
19910
|
-
const session = result2.session;
|
|
19911
|
-
if (session?.title?.trim() && session.summary?.trim()) {
|
|
19912
|
-
const repo = exchange.project?.kind === "code" && exchange.project.repository ? ` [${exchange.project.repository}]` : "";
|
|
19913
|
-
this.upsert({
|
|
19914
|
-
kind: "session",
|
|
19915
|
-
key: exchange.conversationId,
|
|
19916
|
-
title: session.title.trim().slice(0, 80) + repo,
|
|
19917
|
-
content: session.summary.trim(),
|
|
19918
|
-
sourceConversationId: exchange.conversationId
|
|
19919
|
-
});
|
|
19920
|
-
}
|
|
19921
|
-
return changes;
|
|
19922
|
-
}
|
|
19923
|
-
async extract(exchange) {
|
|
19924
|
-
const items = this.list().filter((item) => item.kind !== "session").sort((a, b) => Number(b.pinned) - Number(a.pinned) || b.updatedAt - a.updatedAt).slice(0, MAX_MEMORY_PROMPT_ITEMS);
|
|
19925
|
-
const currentSession = this.list("session").find((item) => item.key === exchange.conversationId);
|
|
19926
|
-
const memory = items.length ? items.map((item) => JSON.stringify({ id: item.id, kind: item.kind, key: item.key, title: item.title, content: item.content, pinned: item.pinned })).join("\n") : "(empty)";
|
|
19927
|
-
const context = exchange.project?.kind === "code" && exchange.project.repository ? `Code chat in GitHub repository ${exchange.project.repository}.` : "General chat.";
|
|
19928
|
-
const user = [
|
|
19929
|
-
`CURRENT MEMORY (one JSON object per line):
|
|
19930
|
-
${memory}`,
|
|
19931
|
-
`THIS CHAT: ${context}${currentSession ? ` Summary so far: ${currentSession.content}` : ""}`,
|
|
19932
|
-
`LATEST USER MESSAGE:
|
|
19933
|
-
${exchange.userText.slice(0, 6e3)}`,
|
|
19934
|
-
`ASSISTANT REPLY:
|
|
19935
|
-
${exchange.assistantText.slice(0, 4e3)}`
|
|
19936
|
-
].join("\n\n");
|
|
19937
|
-
let response;
|
|
19938
|
-
try {
|
|
19939
|
-
response = await (this.options.fetchImpl ?? fetch)(openRouterUrl("chat/completions"), {
|
|
19940
|
-
method: "POST",
|
|
19941
|
-
headers: { ...openRouterHeaders(this.apiKey), "Content-Type": "application/json" },
|
|
19942
|
-
body: JSON.stringify({
|
|
19943
|
-
model: this.model,
|
|
19944
|
-
temperature: 0,
|
|
19945
|
-
response_format: { type: "json_object" },
|
|
19946
|
-
messages: [{ role: "system", content: EXTRACTION_PROMPT }, { role: "user", content: user }]
|
|
19947
|
-
}),
|
|
19948
|
-
signal: AbortSignal.timeout(3e4)
|
|
19949
|
-
});
|
|
19950
|
-
} catch {
|
|
19951
|
-
return void 0;
|
|
19952
|
-
}
|
|
19953
|
-
if (!response.ok) return void 0;
|
|
19954
|
-
const body = await response.json().catch(() => void 0);
|
|
19955
|
-
const text = body?.choices?.[0]?.message?.content;
|
|
19956
|
-
if (!text) return void 0;
|
|
19957
|
-
try {
|
|
19958
|
-
return JSON.parse(text.replace(/^```(?:json)?\s*|\s*```$/g, ""));
|
|
19959
|
-
} catch {
|
|
19960
|
-
return void 0;
|
|
19961
|
-
}
|
|
19962
|
-
}
|
|
19963
|
-
};
|
|
19964
|
-
EXTRACTION_PROMPT = `You maintain the long-term memory of one person (the user) for their AI assistant, Aster. The user has many separate chats; memory is how a new chat already knows their world. Read the latest exchange against the current memory and return JSON operations.
|
|
19965
|
-
|
|
19966
|
-
Remember what will matter in future, unrelated chats:
|
|
19967
|
-
- PEOPLE the user deals with: name, role, organization, relationship to the user, which projects they are part of, anything the user said about working with them.
|
|
19968
|
-
- PROJECTS: name, what it is, client or owner, goals, scope, stack, repository, status, key decisions, and ideas the user brainstormed or chose.
|
|
19969
|
-
- FACTS about the user: durable preferences, circumstances (job, freelance work, location if stated), ways of working, tools they use.
|
|
19970
|
-
|
|
19971
|
-
Do not store: one-off task details, things only relevant inside this chat, general knowledge, the assistant's own suggestions the user did not adopt, credentials or secrets, and sensitive personal details (health, finances, etc.) unless the user explicitly asks you to remember them.
|
|
19972
|
-
|
|
19973
|
-
Rules:
|
|
19974
|
-
- If the user explicitly asks to remember something, you MUST store it, with "pinned": true.
|
|
19975
|
-
- Prefer updating an existing card over creating a new one. To update a person or project, reuse its exact "key" and write the complete new content, merging old and new information (max ~120 words).
|
|
19976
|
-
- Keys: "person:<first-last>" or "project:<short-name>", lowercase with hyphens.
|
|
19977
|
-
- Facts are one sentence. To change a fact, return it with its "id" and new content; to remove an outdated or contradicted fact, put its id in "deletes".
|
|
19978
|
-
- Most exchanges contain nothing worth remembering: return empty "upserts" then. Never invent details.
|
|
19979
|
-
- Always write "session": a short title (max 8 words) and a 1-2 sentence summary of what this chat is about and where it stands, updated with the latest exchange.
|
|
19980
|
-
|
|
19981
|
-
Return only JSON:
|
|
19982
|
-
{"upserts":[{"kind":"person|project|fact","key":"person:...|project:...|null","id":"existing fact id|null","title":"Display name","content":"...","pinned":false}],"deletes":["id"],"session":{"title":"...","summary":"..."}}`;
|
|
19983
|
-
STOP_WORDS = /* @__PURE__ */ new Set(["the", "and", "for", "app", "project", "new", "with", "website", "site"]);
|
|
19984
|
-
}
|
|
19985
|
-
});
|
|
19986
|
-
|
|
19987
|
-
// src/server/aster-github-proxy.ts
|
|
19988
|
-
import { createServer } from "http";
|
|
19989
|
-
import { request as httpsRequest } from "https";
|
|
19990
|
-
import { request as httpRequest } from "http";
|
|
19991
|
-
function extractToken(req) {
|
|
19992
|
-
const header = req.headers.authorization;
|
|
19993
|
-
if (!header) return void 0;
|
|
19994
|
-
const [scheme, value = ""] = header.split(" ", 2);
|
|
19995
|
-
if (/^(bearer|token)$/i.test(scheme ?? "")) return value.trim() || void 0;
|
|
19996
|
-
if (/^basic$/i.test(scheme ?? "")) {
|
|
19997
|
-
const decoded = Buffer.from(value, "base64").toString("utf8");
|
|
19998
|
-
const password = decoded.slice(decoded.indexOf(":") + 1);
|
|
19999
|
-
return password || void 0;
|
|
20000
|
-
}
|
|
20001
|
-
return void 0;
|
|
20002
|
-
}
|
|
20003
|
-
function deny(res, status, message2) {
|
|
20004
|
-
res.writeHead(status, { "content-type": "application/json" });
|
|
20005
|
-
res.end(JSON.stringify({ message: message2 }));
|
|
20006
|
-
}
|
|
20007
|
-
var GIT_PATH_RE, API_PATH_RE, FORWARDED_REQUEST_HEADERS, HOP_BY_HOP, AsterGitHubProxy;
|
|
20008
|
-
var init_aster_github_proxy = __esm({
|
|
20009
|
-
"src/server/aster-github-proxy.ts"() {
|
|
20010
|
-
"use strict";
|
|
20011
|
-
GIT_PATH_RE = /^\/git\/([A-Za-z0-9-]{1,39})\/([A-Za-z0-9._-]{1,100})\.git\/(info\/refs|git-upload-pack|git-receive-pack)$/;
|
|
20012
|
-
API_PATH_RE = /^\/api\/repos\/([A-Za-z0-9-]{1,39})\/([A-Za-z0-9._-]{1,100})(\/.*)?$/;
|
|
20013
|
-
FORWARDED_REQUEST_HEADERS = ["accept", "content-type", "content-encoding", "content-length", "git-protocol", "user-agent", "x-github-api-version"];
|
|
20014
|
-
HOP_BY_HOP = /* @__PURE__ */ new Set(["connection", "keep-alive", "transfer-encoding", "upgrade", "proxy-authenticate", "proxy-authorization", "te", "trailer"]);
|
|
20015
|
-
AsterGitHubProxy = class {
|
|
20016
|
-
constructor(options) {
|
|
20017
|
-
this.options = options;
|
|
20018
|
-
this.gitUpstream = new URL(options.gitUpstream ?? "https://github.com");
|
|
20019
|
-
this.apiUpstream = new URL(options.apiUpstream ?? "https://api.github.com");
|
|
20020
|
-
}
|
|
20021
|
-
options;
|
|
20022
|
-
server;
|
|
20023
|
-
gitUpstream;
|
|
20024
|
-
apiUpstream;
|
|
20025
|
-
async listen(port, host) {
|
|
20026
|
-
this.server = createServer((req, res) => this.handle(req, res));
|
|
20027
|
-
await new Promise((resolve19, reject) => {
|
|
20028
|
-
this.server.once("error", reject);
|
|
20029
|
-
this.server.listen(port, host, () => resolve19());
|
|
20030
|
-
});
|
|
20031
|
-
const address = this.server.address();
|
|
20032
|
-
return typeof address === "object" && address ? address.port : port;
|
|
20033
|
-
}
|
|
20034
|
-
close() {
|
|
20035
|
-
this.server?.close();
|
|
20036
|
-
this.server = void 0;
|
|
20037
|
-
}
|
|
20038
|
-
handle(req, res) {
|
|
20039
|
-
const url = new URL(req.url ?? "/", "http://proxy.local");
|
|
20040
|
-
const git4 = url.pathname.match(GIT_PATH_RE);
|
|
20041
|
-
const api = git4 ? null : url.pathname.match(API_PATH_RE);
|
|
20042
|
-
if (!git4 && !api) return deny(res, 404, "Only this conversation's repository is reachable");
|
|
20043
|
-
const grantToken = extractToken(req);
|
|
20044
|
-
if (!grantToken) {
|
|
20045
|
-
res.writeHead(401, { "www-authenticate": 'Basic realm="aster-github"', "content-type": "text/plain" });
|
|
20046
|
-
res.end("credentials required");
|
|
20047
|
-
return;
|
|
20048
|
-
}
|
|
20049
|
-
const grant = this.options.resolveGrant(grantToken);
|
|
20050
|
-
if (!grant) return deny(res, 403, "This repository grant is invalid or was revoked");
|
|
20051
|
-
const [, owner, name] = git4 ?? api;
|
|
20052
|
-
if (`${owner}/${name}`.toLowerCase() !== grant.repository.toLowerCase()) {
|
|
20053
|
-
return deny(res, 403, `This conversation can only access ${grant.repository}`);
|
|
20054
|
-
}
|
|
20055
|
-
const upstream = git4 ? new URL(`/${owner}/${name}.git/${git4[3]}${url.search}`, this.gitUpstream) : new URL(`/repos/${owner}/${name}${api[3] ?? ""}${url.search}`, this.apiUpstream);
|
|
20056
|
-
const headers = {};
|
|
20057
|
-
for (const name2 of FORWARDED_REQUEST_HEADERS) {
|
|
20058
|
-
const value = req.headers[name2];
|
|
20059
|
-
if (typeof value === "string") headers[name2] = value;
|
|
20060
|
-
}
|
|
20061
|
-
headers["user-agent"] ??= "autopilot-aster";
|
|
20062
|
-
headers.authorization = git4 ? "Basic " + Buffer.from(`x-access-token:${this.options.token}`).toString("base64") : `Bearer ${this.options.token}`;
|
|
20063
|
-
const send = upstream.protocol === "https:" ? httpsRequest : httpRequest;
|
|
20064
|
-
const forward = send(upstream, { method: req.method, headers }, (upstreamRes) => {
|
|
20065
|
-
const responseHeaders = {};
|
|
20066
|
-
for (const [key, value] of Object.entries(upstreamRes.headers)) {
|
|
20067
|
-
if (value !== void 0 && !HOP_BY_HOP.has(key)) responseHeaders[key] = value;
|
|
20068
|
-
}
|
|
20069
|
-
res.writeHead(upstreamRes.statusCode ?? 502, responseHeaders);
|
|
20070
|
-
upstreamRes.pipe(res);
|
|
20071
|
-
});
|
|
20072
|
-
forward.on("error", () => {
|
|
20073
|
-
if (!res.headersSent) deny(res, 502, "GitHub could not be reached");
|
|
20074
|
-
else res.destroy();
|
|
20075
|
-
});
|
|
20076
|
-
req.pipe(forward);
|
|
20077
|
-
}
|
|
20078
|
-
};
|
|
20079
|
-
}
|
|
20080
|
-
});
|
|
20081
|
-
|
|
20082
|
-
// src/server/aster-push.ts
|
|
20083
|
-
import Database6 from "better-sqlite3";
|
|
20084
|
-
import { createSign } from "crypto";
|
|
20085
|
-
import { readFileSync as readFileSync6 } from "fs";
|
|
20086
|
-
import { connect } from "http2";
|
|
20087
|
-
function truncate2(value, limit) {
|
|
20088
|
-
const clean = value.replace(/\s+/g, " ").trim();
|
|
20089
|
-
return clean.length > limit ? clean.slice(0, limit - 1) + "\u2026" : clean;
|
|
20090
|
-
}
|
|
20091
|
-
var DEVICE_TOKEN_RE, AsterPush;
|
|
20092
|
-
var init_aster_push = __esm({
|
|
20093
|
-
"src/server/aster-push.ts"() {
|
|
20094
|
-
"use strict";
|
|
20095
|
-
init_aster_store();
|
|
20096
|
-
DEVICE_TOKEN_RE = /^[0-9a-f]{64,200}$/i;
|
|
20097
|
-
AsterPush = class {
|
|
20098
|
-
db;
|
|
20099
|
-
options;
|
|
20100
|
-
privateKey;
|
|
20101
|
-
jwt;
|
|
20102
|
-
sessions = /* @__PURE__ */ new Map();
|
|
20103
|
-
constructor(dbPath = defaultAsterDbPath(), options = {}, env2 = process.env) {
|
|
20104
|
-
this.options = {
|
|
20105
|
-
...options,
|
|
20106
|
-
keyPath: options.keyPath ?? env2.AUTOPILOT_ASTER_APNS_KEY_PATH,
|
|
20107
|
-
keyId: options.keyId ?? env2.AUTOPILOT_ASTER_APNS_KEY_ID ?? "",
|
|
20108
|
-
teamId: options.teamId ?? env2.AUTOPILOT_ASTER_APNS_TEAM_ID ?? "",
|
|
20109
|
-
topic: options.topic ?? env2.AUTOPILOT_ASTER_APNS_TOPIC ?? "com.aster.ai"
|
|
20110
|
-
};
|
|
20111
|
-
this.db = new Database6(dbPath);
|
|
20112
|
-
this.db.pragma("journal_mode = WAL");
|
|
20113
|
-
this.db.pragma("busy_timeout = 5000");
|
|
20114
|
-
this.db.exec(`
|
|
20115
|
-
CREATE TABLE IF NOT EXISTS aster_devices (
|
|
20116
|
-
token TEXT PRIMARY KEY,
|
|
20117
|
-
environment TEXT NOT NULL,
|
|
20118
|
-
created_at INTEGER NOT NULL,
|
|
20119
|
-
updated_at INTEGER NOT NULL
|
|
20120
|
-
);
|
|
20121
|
-
`);
|
|
20122
|
-
}
|
|
20123
|
-
/** Whether an APNs key is configured; devices are still recorded without one. */
|
|
20124
|
-
get configured() {
|
|
20125
|
-
return Boolean((this.options.keyPath || this.options.deliver) && this.options.keyId && this.options.teamId);
|
|
20126
|
-
}
|
|
20127
|
-
register(token, environment) {
|
|
20128
|
-
if (!DEVICE_TOKEN_RE.test(token)) return false;
|
|
20129
|
-
const now2 = Date.now();
|
|
20130
|
-
this.db.prepare(`INSERT INTO aster_devices (token, environment, created_at, updated_at) VALUES (?, ?, ?, ?)
|
|
20131
|
-
ON CONFLICT(token) DO UPDATE SET environment = excluded.environment, updated_at = excluded.updated_at`).run(token.toLowerCase(), environment, now2, now2);
|
|
20132
|
-
return true;
|
|
20133
|
-
}
|
|
20134
|
-
unregister(token) {
|
|
20135
|
-
this.db.prepare("DELETE FROM aster_devices WHERE token = ?").run(token.toLowerCase());
|
|
20136
|
-
}
|
|
20137
|
-
deviceCount() {
|
|
20138
|
-
return this.db.prepare("SELECT COUNT(*) AS n FROM aster_devices").get().n;
|
|
20139
|
-
}
|
|
20140
|
-
/** Sends to every registered device; tokens Apple reports as gone are removed. Never throws. */
|
|
20141
|
-
async notify(message2) {
|
|
20142
|
-
if (!this.configured) return;
|
|
20143
|
-
const devices = this.db.prepare("SELECT token, environment FROM aster_devices").all();
|
|
20144
|
-
if (devices.length === 0) return;
|
|
20145
|
-
const payload = JSON.stringify({
|
|
20146
|
-
aps: {
|
|
20147
|
-
alert: { title: truncate2(message2.title, 80), body: truncate2(message2.body, 180) },
|
|
20148
|
-
sound: "default",
|
|
20149
|
-
"thread-id": message2.conversationId,
|
|
20150
|
-
...message2.category ? { category: message2.category } : {}
|
|
20151
|
-
},
|
|
20152
|
-
conversationId: message2.conversationId,
|
|
20153
|
-
...message2.approvalId ? { approvalId: message2.approvalId } : {}
|
|
20154
|
-
});
|
|
20155
|
-
await Promise.all(devices.map(async (device) => {
|
|
20156
|
-
const host = device.environment === "development" ? "api.sandbox.push.apple.com" : "api.push.apple.com";
|
|
20157
|
-
try {
|
|
20158
|
-
const status = await this.send(host, device.token, payload);
|
|
20159
|
-
if (status === 410 || status === 400) this.unregister(device.token);
|
|
20160
|
-
} catch {
|
|
20161
|
-
}
|
|
20162
|
-
}));
|
|
20163
|
-
}
|
|
20164
|
-
close() {
|
|
20165
|
-
for (const session of this.sessions.values()) session.close();
|
|
20166
|
-
this.sessions.clear();
|
|
20167
|
-
this.db.close();
|
|
20168
|
-
}
|
|
20169
|
-
async send(host, token, payload) {
|
|
20170
|
-
const headers = {
|
|
20171
|
-
authorization: `bearer ${this.token()}`,
|
|
20172
|
-
"apns-topic": this.options.topic,
|
|
20173
|
-
"apns-push-type": "alert",
|
|
20174
|
-
"apns-priority": "10"
|
|
20175
|
-
};
|
|
20176
|
-
if (this.options.deliver) return this.options.deliver({ host, token, headers, payload });
|
|
20177
|
-
const session = this.session(host);
|
|
20178
|
-
return await new Promise((resolve19, reject) => {
|
|
20179
|
-
const stream = session.request({ ":method": "POST", ":path": `/3/device/${token}`, ...headers, "content-type": "application/json" });
|
|
20180
|
-
let status = 0;
|
|
20181
|
-
stream.setTimeout(15e3, () => stream.close());
|
|
20182
|
-
stream.on("response", (responseHeaders) => {
|
|
20183
|
-
status = Number(responseHeaders[":status"]) || 0;
|
|
20184
|
-
});
|
|
20185
|
-
stream.on("error", reject);
|
|
20186
|
-
stream.on("close", () => resolve19(status));
|
|
20187
|
-
stream.resume();
|
|
20188
|
-
stream.end(payload);
|
|
20189
|
-
});
|
|
20190
|
-
}
|
|
20191
|
-
session(host) {
|
|
20192
|
-
const existing = this.sessions.get(host);
|
|
20193
|
-
if (existing && !existing.closed && !existing.destroyed) return existing;
|
|
20194
|
-
const session = connect(`https://${host}`);
|
|
20195
|
-
session.on("error", () => this.sessions.delete(host));
|
|
20196
|
-
session.on("close", () => this.sessions.delete(host));
|
|
20197
|
-
session.unref();
|
|
20198
|
-
this.sessions.set(host, session);
|
|
20199
|
-
return session;
|
|
20200
|
-
}
|
|
20201
|
-
/** Provider token (ES256 JWT); Apple accepts one for up to an hour, so it is reused for 50 minutes. */
|
|
20202
|
-
token() {
|
|
20203
|
-
const now2 = Math.floor(Date.now() / 1e3);
|
|
20204
|
-
if (this.jwt && now2 - this.jwt.issuedAt < 50 * 60) return this.jwt.value;
|
|
20205
|
-
this.privateKey ??= readFileSync6(this.options.keyPath, "utf8");
|
|
20206
|
-
const encode = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
20207
|
-
const unsigned = `${encode({ alg: "ES256", kid: this.options.keyId })}.${encode({ iss: this.options.teamId, iat: now2 })}`;
|
|
20208
|
-
const signature = createSign("SHA256").update(unsigned).sign({ key: this.privateKey, dsaEncoding: "ieee-p1363" }).toString("base64url");
|
|
20209
|
-
this.jwt = { value: `${unsigned}.${signature}`, issuedAt: now2 };
|
|
20210
|
-
return this.jwt.value;
|
|
20211
|
-
}
|
|
20212
|
-
};
|
|
20213
|
-
}
|
|
20214
|
-
});
|
|
20215
|
-
|
|
20216
19442
|
// src/server/aster-api.ts
|
|
20217
|
-
import { randomUUID as
|
|
19443
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
20218
19444
|
import { unlink as unlink2 } from "fs/promises";
|
|
20219
19445
|
import { resolve as resolve13 } from "path";
|
|
20220
19446
|
function parseCursor(header, query) {
|
|
@@ -20223,23 +19449,8 @@ function parseCursor(header, query) {
|
|
|
20223
19449
|
const cursor = Number(value);
|
|
20224
19450
|
return Number.isSafeInteger(cursor) ? cursor : void 0;
|
|
20225
19451
|
}
|
|
20226
|
-
function
|
|
20227
|
-
let text = "";
|
|
20228
|
-
let cursor = after;
|
|
20229
|
-
for (; ; ) {
|
|
20230
|
-
const events = store.eventsAfter(conversationId, cursor, 500);
|
|
20231
|
-
if (events.length === 0) break;
|
|
20232
|
-
for (const event of events) {
|
|
20233
|
-
if (event.runId === runId && event.type === "assistant.delta" && typeof event.data.text === "string") text += event.data.text;
|
|
20234
|
-
}
|
|
20235
|
-
cursor = events.at(-1).sequence;
|
|
20236
|
-
if (text.length > 2e4) break;
|
|
20237
|
-
}
|
|
20238
|
-
return text;
|
|
20239
|
-
}
|
|
20240
|
-
function publicConversation(conversation, project) {
|
|
19452
|
+
function publicConversation(conversation) {
|
|
20241
19453
|
return {
|
|
20242
|
-
...project ? { kind: project.kind, repository: project.repository, branch: project.branch } : {},
|
|
20243
19454
|
conversationId: conversation.id,
|
|
20244
19455
|
workspaceId: conversation.workspaceId,
|
|
20245
19456
|
model: conversation.model,
|
|
@@ -20263,25 +19474,6 @@ function publicApproval(approval) {
|
|
|
20263
19474
|
decision: approval.decision
|
|
20264
19475
|
};
|
|
20265
19476
|
}
|
|
20266
|
-
async function selectableModels(config2) {
|
|
20267
|
-
if (config2.modelCatalog === "fixed") return config2.models.map((id) => ({ id }));
|
|
20268
|
-
const catalog = await loadOpenRouterCatalog();
|
|
20269
|
-
return catalog.filter((model) => model.supports.tools).map((model) => ({
|
|
20270
|
-
id: model.id,
|
|
20271
|
-
...model.name ? { name: model.name } : {},
|
|
20272
|
-
contextWindow: model.contextWindow,
|
|
20273
|
-
inputPerMtok: model.pricing.inputPerMtok,
|
|
20274
|
-
outputPerMtok: model.pricing.outputPerMtok,
|
|
20275
|
-
...model.created !== void 0 ? { created: model.created } : {},
|
|
20276
|
-
reasoning: model.supports.reasoning,
|
|
20277
|
-
vision: model.supports.vision === true
|
|
20278
|
-
}));
|
|
20279
|
-
}
|
|
20280
|
-
async function isSelectableModel(config2, model) {
|
|
20281
|
-
if (!model.trim() || model.length > 200) return false;
|
|
20282
|
-
if (config2.modelCatalog === "fixed") return config2.models.includes(model);
|
|
20283
|
-
return (await selectableModels(config2)).some((entry) => entry.id === model);
|
|
20284
|
-
}
|
|
20285
19477
|
async function readJsonBody(req) {
|
|
20286
19478
|
const chunks = [];
|
|
20287
19479
|
let bytes = 0;
|
|
@@ -20313,27 +19505,23 @@ function json(res, status, data) {
|
|
|
20313
19505
|
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
|
|
20314
19506
|
res.end(JSON.stringify(data));
|
|
20315
19507
|
}
|
|
20316
|
-
var API_PREFIX, MAX_BODY_BYTES, MAX_USER_TURN_CHARS, CLIENT_TURN_ID_RE,
|
|
19508
|
+
var API_PREFIX, MAX_BODY_BYTES, MAX_USER_TURN_CHARS, CLIENT_TURN_ID_RE, MAX_APPROVAL_ARG_BYTES, CONVERSATION_ID_RE, ALLOWED_ASTER_TOOLS, AsterApi;
|
|
20317
19509
|
var init_aster_api = __esm({
|
|
20318
19510
|
"src/server/aster-api.ts"() {
|
|
20319
19511
|
"use strict";
|
|
19512
|
+
init_system_prompt();
|
|
20320
19513
|
init_executor();
|
|
20321
19514
|
init_store();
|
|
19515
|
+
init_worktrees();
|
|
20322
19516
|
init_sessions();
|
|
20323
19517
|
init_version();
|
|
20324
19518
|
init_aster_store();
|
|
20325
19519
|
init_aster_workspaces();
|
|
20326
|
-
init_openrouter_catalog();
|
|
20327
|
-
init_aster_github();
|
|
20328
|
-
init_aster_memory();
|
|
20329
|
-
init_aster_github_proxy();
|
|
20330
|
-
init_aster_push();
|
|
20331
19520
|
init_aster_tools();
|
|
20332
19521
|
API_PREFIX = "/api/v1";
|
|
20333
|
-
MAX_BODY_BYTES =
|
|
20334
|
-
MAX_USER_TURN_CHARS =
|
|
19522
|
+
MAX_BODY_BYTES = 64 * 1024;
|
|
19523
|
+
MAX_USER_TURN_CHARS = 2e4;
|
|
20335
19524
|
CLIENT_TURN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
20336
|
-
CREATE_IDEMPOTENCY_KEY_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
20337
19525
|
MAX_APPROVAL_ARG_BYTES = 24 * 1024;
|
|
20338
19526
|
CONVERSATION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
20339
19527
|
ALLOWED_ASTER_TOOLS = ["read", "write", "edit"];
|
|
@@ -20341,62 +19529,11 @@ var init_aster_api = __esm({
|
|
|
20341
19529
|
store;
|
|
20342
19530
|
recovery;
|
|
20343
19531
|
waiters = /* @__PURE__ */ new Map();
|
|
20344
|
-
connectedEventStreams = /* @__PURE__ */ new Map();
|
|
20345
19532
|
runtime;
|
|
20346
19533
|
config;
|
|
20347
|
-
|
|
20348
|
-
memory;
|
|
20349
|
-
constructor(config2, runtime, github = new AsterGitHub(), memory, push) {
|
|
19534
|
+
constructor(config2, runtime) {
|
|
20350
19535
|
this.config = config2;
|
|
20351
19536
|
this.runtime = runtime;
|
|
20352
|
-
this.github = github;
|
|
20353
|
-
this.memory = memory;
|
|
20354
|
-
this.push = push;
|
|
20355
|
-
}
|
|
20356
|
-
githubProxy;
|
|
20357
|
-
/** Where cells reach the repository-scoped GitHub proxy, when it is running. */
|
|
20358
|
-
githubProxyUrl;
|
|
20359
|
-
/**
|
|
20360
|
-
* Starts the repository-scoped GitHub proxy when configured
|
|
20361
|
-
* (AUTOPILOT_ASTER_GITHUB_PROXY_LISTEN=host:port, AUTOPILOT_ASTER_GITHUB_PROXY_URL as cells see it).
|
|
20362
|
-
*/
|
|
20363
|
-
async startGitHubProxy(env2 = process.env) {
|
|
20364
|
-
const listen = env2.AUTOPILOT_ASTER_GITHUB_PROXY_LISTEN;
|
|
20365
|
-
const publicUrl = env2.AUTOPILOT_ASTER_GITHUB_PROXY_URL;
|
|
20366
|
-
const token = env2.GITHUB_TOKEN;
|
|
20367
|
-
if (!listen || !publicUrl || !token || this.githubProxy) return;
|
|
20368
|
-
const separator = listen.lastIndexOf(":");
|
|
20369
|
-
const proxy = new AsterGitHubProxy({ token, resolveGrant: (grant) => this.getStore().resolveGitHubGrant(grant) });
|
|
20370
|
-
await proxy.listen(Number(listen.slice(separator + 1)), listen.slice(0, separator));
|
|
20371
|
-
this.githubProxy = proxy;
|
|
20372
|
-
this.githubProxyUrl = publicUrl.replace(/\/+$/, "");
|
|
20373
|
-
}
|
|
20374
|
-
push;
|
|
20375
|
-
getPush() {
|
|
20376
|
-
try {
|
|
20377
|
-
return this.push ??= new AsterPush();
|
|
20378
|
-
} catch {
|
|
20379
|
-
return void 0;
|
|
20380
|
-
}
|
|
20381
|
-
}
|
|
20382
|
-
/** "Your task finished" (or failed) on the user's devices; cancelled turns stay silent. */
|
|
20383
|
-
notifyTurnEnd(store, conversationId, runId, status, reason, userText, after) {
|
|
20384
|
-
if (status !== "completed" && status !== "failed") return;
|
|
20385
|
-
if ((this.connectedEventStreams.get(conversationId) ?? 0) > 0) return;
|
|
20386
|
-
const push = this.getPush();
|
|
20387
|
-
if (!push?.configured) return;
|
|
20388
|
-
const reply = status === "completed" ? collectAssistantText(store, conversationId, runId, after).trim() : "";
|
|
20389
|
-
const title = userText.trim().split("\n")[0] || "Aster";
|
|
20390
|
-
const body = status === "completed" ? reply ? `\u2705 ${reply}` : "\u2705 Done" : `\u26A0\uFE0F Couldn\u2019t finish${reason && reason !== "error" ? `: ${reason}` : ""}`;
|
|
20391
|
-
void push.notify({ title, body, conversationId });
|
|
20392
|
-
}
|
|
20393
|
-
getMemory() {
|
|
20394
|
-
if (process.env.AUTOPILOT_ASTER_MEMORY === "off") return void 0;
|
|
20395
|
-
try {
|
|
20396
|
-
return this.memory ??= new AsterMemory();
|
|
20397
|
-
} catch {
|
|
20398
|
-
return void 0;
|
|
20399
|
-
}
|
|
20400
19537
|
}
|
|
20401
19538
|
async handle(req, res) {
|
|
20402
19539
|
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
@@ -20418,7 +19555,7 @@ var init_aster_api = __esm({
|
|
|
20418
19555
|
service: "autopilot",
|
|
20419
19556
|
apiVersion: "v1",
|
|
20420
19557
|
serviceVersion: getAppVersion(),
|
|
20421
|
-
capabilities: ["workspaces", "conversations", "turns", "event-replay", "cancellation", "human-approvals"
|
|
19558
|
+
capabilities: ["workspaces", "conversations", "turns", "event-replay", "cancellation", "human-approvals"]
|
|
20422
19559
|
});
|
|
20423
19560
|
}
|
|
20424
19561
|
if (path === `${API_PREFIX}/workspaces` && method === "GET") {
|
|
@@ -20429,86 +19566,23 @@ var init_aster_api = __esm({
|
|
|
20429
19566
|
}
|
|
20430
19567
|
if (path === `${API_PREFIX}/models` && method === "GET") {
|
|
20431
19568
|
if (!this.hasScope(principal, "models:read")) return sendError(res, 403, "insufficient_scope", "Credential lacks model/read scope");
|
|
20432
|
-
|
|
20433
|
-
if (models.length === 0) return sendError(res, 503, "model_catalog_unavailable", "The model catalog is unavailable; try again shortly");
|
|
20434
|
-
return json(res, 200, { models: models.map((model) => model.id), catalog: models });
|
|
19569
|
+
return json(res, 200, { models: config2.models });
|
|
20435
19570
|
}
|
|
20436
19571
|
if (path === `${API_PREFIX}/conversations` && method === "POST") {
|
|
20437
19572
|
if (!this.hasScope(principal, "conversations:write")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/write scope");
|
|
20438
19573
|
const body = await readJsonBody(req);
|
|
20439
|
-
if (Object.keys(body).some((key) =>
|
|
20440
|
-
return sendError(res, 400, "unsupported_field", "Conversation creation accepts workspaceId
|
|
20441
|
-
}
|
|
20442
|
-
if (body.kind !== void 0 && body.kind !== "chat" && body.kind !== "code") {
|
|
20443
|
-
return sendError(res, 400, "invalid_kind", 'kind must be "chat" or "code"');
|
|
20444
|
-
}
|
|
20445
|
-
if (body.kind === "code" && (typeof body.repository !== "string" || !REPOSITORY_FULL_NAME_RE.test(body.repository))) {
|
|
20446
|
-
return sendError(res, 400, "repository_required", "Code conversations need a repository as owner/name");
|
|
20447
|
-
}
|
|
20448
|
-
if (body.kind !== "code" && body.repository !== void 0) {
|
|
20449
|
-
return sendError(res, 400, "unsupported_field", "repository is only accepted for code conversations");
|
|
19574
|
+
if (Object.keys(body).some((key) => key !== "workspaceId" && key !== "model")) {
|
|
19575
|
+
return sendError(res, 400, "unsupported_field", "Conversation creation accepts only workspaceId and model; paths are server-configured");
|
|
20450
19576
|
}
|
|
20451
19577
|
if (typeof body.workspaceId !== "string" || !principal.workspaceIds.includes(body.workspaceId)) {
|
|
20452
19578
|
return sendError(res, 403, "workspace_forbidden", "Workspace ID is not in this credential's scope");
|
|
20453
19579
|
}
|
|
20454
19580
|
const workspace = findAsterWorkspace(config2, body.workspaceId);
|
|
20455
19581
|
if (!workspace) return sendError(res, 404, "workspace_not_found", "Workspace is not configured");
|
|
20456
|
-
if (typeof body.model !== "string" || !
|
|
19582
|
+
if (typeof body.model !== "string" || !config2.models.includes(body.model)) {
|
|
20457
19583
|
return sendError(res, 400, "model_not_allowed", "Select a model from the configured model list");
|
|
20458
19584
|
}
|
|
20459
|
-
|
|
20460
|
-
if (typeof idempotencyKey !== "string" || !CREATE_IDEMPOTENCY_KEY_RE.test(idempotencyKey)) {
|
|
20461
|
-
return sendError(res, 400, "idempotency_key_required", "Conversation creation requires an Idempotency-Key header of 1-128 safe characters");
|
|
20462
|
-
}
|
|
20463
|
-
const kind = body.kind;
|
|
20464
|
-
return await this.createConversation(res, store, principal, workspace, body.model, idempotencyKey, kind, body.repository);
|
|
20465
|
-
}
|
|
20466
|
-
if (path === `${API_PREFIX}/devices` && method === "POST") {
|
|
20467
|
-
if (!this.hasScope(principal, "conversations:write")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/write scope");
|
|
20468
|
-
const body = await readJsonBody(req);
|
|
20469
|
-
const environment = body.environment === "development" ? "development" : "production";
|
|
20470
|
-
const push = this.getPush();
|
|
20471
|
-
if (!push || typeof body.token !== "string" || !push.register(body.token, environment)) {
|
|
20472
|
-
return sendError(res, 400, "invalid_device_token", "A hex APNs device token is required");
|
|
20473
|
-
}
|
|
20474
|
-
return json(res, 200, { registered: true, notifications: push.configured });
|
|
20475
|
-
}
|
|
20476
|
-
const deviceMatch = path.match(/^\/api\/v1\/devices\/([0-9a-fA-F]{64,200})$/);
|
|
20477
|
-
if (deviceMatch && method === "DELETE") {
|
|
20478
|
-
if (!this.hasScope(principal, "conversations:write")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/write scope");
|
|
20479
|
-
this.getPush()?.unregister(deviceMatch[1]);
|
|
20480
|
-
return json(res, 200, { unregistered: true });
|
|
20481
|
-
}
|
|
20482
|
-
if (path === `${API_PREFIX}/memory` && method === "GET") {
|
|
20483
|
-
if (!this.hasScope(principal, "conversations:read")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/read scope");
|
|
20484
|
-
const memory = this.getMemory();
|
|
20485
|
-
return json(res, 200, { enabled: Boolean(memory?.enabled), items: memory ? memory.list() : [] });
|
|
20486
|
-
}
|
|
20487
|
-
const memoryMatch = path.match(/^\/api\/v1\/memory\/([^/]+)$/);
|
|
20488
|
-
if (memoryMatch && method === "DELETE") {
|
|
20489
|
-
if (!this.hasScope(principal, "conversations:write")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/write scope");
|
|
20490
|
-
const memory = this.getMemory();
|
|
20491
|
-
if (!memory || !memory.delete(memoryMatch[1])) return sendError(res, 404, "memory_not_found", "Memory item not found");
|
|
20492
|
-
return json(res, 200, { deleted: memoryMatch[1] });
|
|
20493
|
-
}
|
|
20494
|
-
if (path === `${API_PREFIX}/github/repositories` && (method === "GET" || method === "POST")) {
|
|
20495
|
-
if (!this.hasScope(principal, "conversations:write")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/write scope");
|
|
20496
|
-
try {
|
|
20497
|
-
if (method === "GET") {
|
|
20498
|
-
return json(res, 200, { repositories: await this.github.listRepositories(url.searchParams.get("q") ?? "") });
|
|
20499
|
-
}
|
|
20500
|
-
const body = await readJsonBody(req);
|
|
20501
|
-
if (typeof body.name !== "string") return sendError(res, 400, "invalid_repository_name", "name is required");
|
|
20502
|
-
const repository = await this.github.createRepository({
|
|
20503
|
-
name: body.name.trim(),
|
|
20504
|
-
private: body.private !== false,
|
|
20505
|
-
...typeof body.description === "string" ? { description: body.description } : {}
|
|
20506
|
-
});
|
|
20507
|
-
return json(res, 201, { repository });
|
|
20508
|
-
} catch (error) {
|
|
20509
|
-
if (error instanceof AsterGitHubError) return sendError(res, error.status, error.code, error.message);
|
|
20510
|
-
throw error;
|
|
20511
|
-
}
|
|
19585
|
+
return await this.createConversation(res, store, workspace, body.model);
|
|
20512
19586
|
}
|
|
20513
19587
|
const eventsMatch = path.match(/^\/api\/v1\/conversations\/([^/]+)\/events$/);
|
|
20514
19588
|
if (eventsMatch && method === "GET") {
|
|
@@ -20525,7 +19599,7 @@ var init_aster_api = __esm({
|
|
|
20525
19599
|
const conversation = await this.authorizedConversation(store, principal, turnMatch[1]);
|
|
20526
19600
|
if (!conversation) return sendError(res, 404, "conversation_not_found", "Conversation not found");
|
|
20527
19601
|
const body = await readJsonBody(req);
|
|
20528
|
-
if (Object.keys(body).some((key) => key !== "text" && key !== "clientTurnId"
|
|
19602
|
+
if (Object.keys(body).some((key) => key !== "text" && key !== "clientTurnId")) return sendError(res, 400, "unsupported_field", "Turns accept text and an optional clientTurnId; workspace and model are fixed by the conversation");
|
|
20529
19603
|
const hasClientTurnId = Object.hasOwn(body, "clientTurnId");
|
|
20530
19604
|
const clientTurnId = hasClientTurnId && typeof body.clientTurnId === "string" ? body.clientTurnId : void 0;
|
|
20531
19605
|
if (hasClientTurnId && (!clientTurnId || !CLIENT_TURN_ID_RE.test(clientTurnId))) return sendError(res, 400, "invalid_client_turn_id", "clientTurnId must be 1-128 ASCII letters, digits, '.', '_', ':', or '-' and start with a letter or digit");
|
|
@@ -20533,18 +19607,7 @@ var init_aster_api = __esm({
|
|
|
20533
19607
|
const text = requestText.trim();
|
|
20534
19608
|
if (!text || text.length > MAX_USER_TURN_CHARS) return sendError(res, 400, "invalid_turn", `Turn text must be 1-${MAX_USER_TURN_CHARS} characters`);
|
|
20535
19609
|
if (containsLikelyProviderSecret(text)) return sendError(res, 400, "secret_input_rejected", "Provider credentials must not be sent in conversation text");
|
|
20536
|
-
|
|
20537
|
-
if (Object.hasOwn(body, "model")) {
|
|
20538
|
-
if (typeof body.model !== "string" || !await isSelectableModel(config2, body.model)) {
|
|
20539
|
-
return sendError(res, 400, "model_not_allowed", "Select a model from the model catalog");
|
|
20540
|
-
}
|
|
20541
|
-
if (body.model !== conversation.model) {
|
|
20542
|
-
const switched = await this.switchModel(res, store, conversation, body.model);
|
|
20543
|
-
if (!switched) return;
|
|
20544
|
-
target = switched;
|
|
20545
|
-
}
|
|
20546
|
-
}
|
|
20547
|
-
return await this.appendTurn(res, store, target, clientTurnId, requestText, text);
|
|
19610
|
+
return await this.appendTurn(res, store, conversation, clientTurnId, requestText, text);
|
|
20548
19611
|
}
|
|
20549
19612
|
const cancelMatch = path.match(/^\/api\/v1\/conversations\/([^/]+)\/cancel$/);
|
|
20550
19613
|
if (cancelMatch && method === "POST") {
|
|
@@ -20553,80 +19616,12 @@ var init_aster_api = __esm({
|
|
|
20553
19616
|
if (!conversation) return sendError(res, 404, "conversation_not_found", "Conversation not found");
|
|
20554
19617
|
return this.cancelConversation(res, store, conversation);
|
|
20555
19618
|
}
|
|
20556
|
-
const lifecycleMatch = path.match(/^\/api\/v1\/conversations\/([^/]+)\/(pause|resume)$/);
|
|
20557
|
-
if (lifecycleMatch && method === "POST") {
|
|
20558
|
-
if (!this.hasScope(principal, "conversations:write")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/write scope");
|
|
20559
|
-
const conversation = await this.authorizedConversation(store, principal, lifecycleMatch[1]);
|
|
20560
|
-
if (!conversation) return sendError(res, 404, "conversation_not_found", "Conversation not found");
|
|
20561
|
-
return lifecycleMatch[2] === "pause" ? this.pauseConversation(res, store, conversation) : this.resumeConversation(res, store, conversation);
|
|
20562
|
-
}
|
|
20563
|
-
const artifactsMatch = path.match(/^\/api\/v1\/conversations\/([^/]+)\/artifacts(\/file)?$/);
|
|
20564
|
-
if (artifactsMatch && method === "GET") {
|
|
20565
|
-
if (!this.hasScope(principal, "conversations:read")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/read scope");
|
|
20566
|
-
const conversation = await this.authorizedConversation(store, principal, artifactsMatch[1]);
|
|
20567
|
-
if (!conversation) return sendError(res, 404, "conversation_not_found", "Conversation not found");
|
|
20568
|
-
const cell = this.readyCell(res, store, conversation.id);
|
|
20569
|
-
if (!cell) return;
|
|
20570
|
-
if (!this.runtime.listArtifacts || !this.runtime.readArtifact) return sendError(res, 503, "artifacts_unavailable", "Artifacts are not configured");
|
|
20571
|
-
if (!artifactsMatch[2]) return json(res, 200, { artifacts: await this.runtime.listArtifacts(cell) });
|
|
20572
|
-
try {
|
|
20573
|
-
const artifact = await this.runtime.readArtifact(cell, url.searchParams.get("path") ?? "");
|
|
20574
|
-
res.writeHead(200, {
|
|
20575
|
-
"content-type": artifact.mimeType,
|
|
20576
|
-
"content-length": artifact.data.length,
|
|
20577
|
-
"content-disposition": `attachment; filename*=UTF-8''${encodeURIComponent(artifact.name)}`,
|
|
20578
|
-
"cache-control": "no-store",
|
|
20579
|
-
"x-content-type-options": "nosniff"
|
|
20580
|
-
});
|
|
20581
|
-
res.end(artifact.data);
|
|
20582
|
-
return;
|
|
20583
|
-
} catch (error) {
|
|
20584
|
-
const code = error instanceof Error ? error.message : "";
|
|
20585
|
-
if (code === "invalid_artifact_path") return sendError(res, 400, code, "Artifact path is invalid");
|
|
20586
|
-
if (code === "artifact_too_large") return sendError(res, 413, code, "Artifact is larger than 50 MB");
|
|
20587
|
-
return sendError(res, 404, "artifact_not_found", "Artifact not found");
|
|
20588
|
-
}
|
|
20589
|
-
}
|
|
20590
|
-
const checkpointsMatch = path.match(/^\/api\/v1\/conversations\/([^/]+)\/checkpoints$/);
|
|
20591
|
-
if (checkpointsMatch && method === "GET") {
|
|
20592
|
-
if (!this.hasScope(principal, "conversations:read")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/read scope");
|
|
20593
|
-
const conversation = await this.authorizedConversation(store, principal, checkpointsMatch[1]);
|
|
20594
|
-
if (!conversation) return sendError(res, 404, "conversation_not_found", "Conversation not found");
|
|
20595
|
-
const cell = this.readyCell(res, store, conversation.id);
|
|
20596
|
-
if (!cell) return;
|
|
20597
|
-
if (!this.runtime.listCheckpoints) return sendError(res, 503, "checkpoints_unavailable", "Checkpoints are not configured");
|
|
20598
|
-
return json(res, 200, { checkpoints: await this.runtime.listCheckpoints(cell) });
|
|
20599
|
-
}
|
|
20600
|
-
const restoreMatch = path.match(/^\/api\/v1\/conversations\/([^/]+)\/checkpoints\/([0-9a-f]{7,40})\/restore$/);
|
|
20601
|
-
if (restoreMatch && method === "POST") {
|
|
20602
|
-
if (!this.hasScope(principal, "conversations:write")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/write scope");
|
|
20603
|
-
const conversation = await this.authorizedConversation(store, principal, restoreMatch[1]);
|
|
20604
|
-
if (!conversation) return sendError(res, 404, "conversation_not_found", "Conversation not found");
|
|
20605
|
-
if (conversation.activeRunId) return sendError(res, 409, "turn_active", "Wait for the current turn to finish or cancel it before restoring");
|
|
20606
|
-
const cell = this.readyCell(res, store, conversation.id);
|
|
20607
|
-
if (!cell) return;
|
|
20608
|
-
if (!this.runtime.restoreCheckpoint) return sendError(res, 503, "checkpoints_unavailable", "Checkpoints are not configured");
|
|
20609
|
-
try {
|
|
20610
|
-
const checkpoint = await this.runtime.restoreCheckpoint(cell, conversation.id, restoreMatch[2]);
|
|
20611
|
-
store.appendEvent(conversation.id, null, "checkpoint.restored", { restoredFrom: restoreMatch[2], ...checkpoint });
|
|
20612
|
-
return json(res, 200, { checkpoint });
|
|
20613
|
-
} catch (error) {
|
|
20614
|
-
if (error instanceof Error && error.message === "checkpoint_not_found") return sendError(res, 404, "checkpoint_not_found", "Checkpoint not found");
|
|
20615
|
-
return sendError(res, 500, "checkpoint_restore_failed", "The workspace could not be restored");
|
|
20616
|
-
}
|
|
20617
|
-
}
|
|
20618
19619
|
const conversationMatch = path.match(/^\/api\/v1\/conversations\/([^/]+)$/);
|
|
20619
|
-
if (conversationMatch && method === "DELETE") {
|
|
20620
|
-
if (!this.hasScope(principal, "conversations:write")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/write scope");
|
|
20621
|
-
const conversation = await this.authorizedConversation(store, principal, conversationMatch[1]);
|
|
20622
|
-
if (!conversation) return sendError(res, 404, "conversation_not_found", "Conversation not found");
|
|
20623
|
-
return this.destroyConversation(res, store, conversation);
|
|
20624
|
-
}
|
|
20625
19620
|
if (conversationMatch && method === "GET") {
|
|
20626
19621
|
if (!this.hasScope(principal, "conversations:read")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/read scope");
|
|
20627
19622
|
const conversation = await this.authorizedConversation(store, principal, conversationMatch[1]);
|
|
20628
19623
|
if (!conversation) return sendError(res, 404, "conversation_not_found", "Conversation not found");
|
|
20629
|
-
return json(res, 200, publicConversation(conversation
|
|
19624
|
+
return json(res, 200, publicConversation(conversation));
|
|
20630
19625
|
}
|
|
20631
19626
|
const approvalMatch = path.match(/^\/api\/v1\/approvals\/([^/]+)$/);
|
|
20632
19627
|
if (approvalMatch && method === "GET") {
|
|
@@ -20634,8 +19629,7 @@ var init_aster_api = __esm({
|
|
|
20634
19629
|
const approval = store.getApproval(approvalMatch[1]);
|
|
20635
19630
|
if (!approval) return sendError(res, 404, "approval_not_found", "Approval not found");
|
|
20636
19631
|
const conversation = store.getConversation(approval.conversationId);
|
|
20637
|
-
|
|
20638
|
-
if (!conversation || mapping?.credentialId !== principal.credentialId || !principal.workspaceIds.includes(conversation.workspaceId)) return sendError(res, 404, "approval_not_found", "Approval not found");
|
|
19632
|
+
if (!conversation || !principal.workspaceIds.includes(conversation.workspaceId)) return sendError(res, 404, "approval_not_found", "Approval not found");
|
|
20639
19633
|
return json(res, 200, publicApproval(approval));
|
|
20640
19634
|
}
|
|
20641
19635
|
if (approvalMatch && method === "POST") {
|
|
@@ -20643,8 +19637,7 @@ var init_aster_api = __esm({
|
|
|
20643
19637
|
const approval = store.getApproval(approvalMatch[1]);
|
|
20644
19638
|
if (!approval) return sendError(res, 404, "approval_not_found", "Approval not found");
|
|
20645
19639
|
const conversation = store.getConversation(approval.conversationId);
|
|
20646
|
-
|
|
20647
|
-
if (!conversation || mapping?.credentialId !== principal.credentialId || !principal.workspaceIds.includes(conversation.workspaceId)) return sendError(res, 404, "approval_not_found", "Approval not found");
|
|
19640
|
+
if (!conversation || !principal.workspaceIds.includes(conversation.workspaceId)) return sendError(res, 404, "approval_not_found", "Approval not found");
|
|
20648
19641
|
const body = await readJsonBody(req);
|
|
20649
19642
|
if (Object.keys(body).some((key) => key !== "decision")) return sendError(res, 400, "unsupported_field", "Approval resolution accepts decision only");
|
|
20650
19643
|
if (body.decision !== "allow" && body.decision !== "deny") return sendError(res, 400, "invalid_approval_decision", "decision must be allow or deny");
|
|
@@ -20665,12 +19658,6 @@ var init_aster_api = __esm({
|
|
|
20665
19658
|
}
|
|
20666
19659
|
this.store?.close();
|
|
20667
19660
|
this.store = void 0;
|
|
20668
|
-
this.memory?.close();
|
|
20669
|
-
this.memory = void 0;
|
|
20670
|
-
this.githubProxy?.close();
|
|
20671
|
-
this.githubProxy = void 0;
|
|
20672
|
-
this.push?.close();
|
|
20673
|
-
this.push = void 0;
|
|
20674
19661
|
}
|
|
20675
19662
|
getStore() {
|
|
20676
19663
|
return this.store ??= new AsterStore();
|
|
@@ -20681,8 +19668,7 @@ var init_aster_api = __esm({
|
|
|
20681
19668
|
async authorizedConversation(store, principal, id) {
|
|
20682
19669
|
if (!CONVERSATION_ID_RE.test(id)) return void 0;
|
|
20683
19670
|
const conversation = store.getConversation(id);
|
|
20684
|
-
|
|
20685
|
-
if (!conversation || mapping?.credentialId !== principal.credentialId || !principal.workspaceIds.includes(conversation.workspaceId)) return void 0;
|
|
19671
|
+
if (!conversation || !principal.workspaceIds.includes(conversation.workspaceId)) return void 0;
|
|
20686
19672
|
const activeRunId = conversation.activeRunId;
|
|
20687
19673
|
if (activeRunId) {
|
|
20688
19674
|
const runs = new RunStore();
|
|
@@ -20722,14 +19708,6 @@ var init_aster_api = __esm({
|
|
|
20722
19708
|
} else if (run.status === "needs_input") {
|
|
20723
19709
|
runs.transition(run.id, "cancelled", "approval_expired_on_restart");
|
|
20724
19710
|
store.updateConversationStatus(conversation.id, run.id, "interrupted", { reason: "approval_expired_on_restart" });
|
|
20725
|
-
} else {
|
|
20726
|
-
const mapping = store.getCellMapping(conversation.id);
|
|
20727
|
-
if (mapping?.cellId && this.runtime.resumeTurn) {
|
|
20728
|
-
this.runtime.resumeTurn(this.cellTurn(store, conversation, run.id, mapping.cellId, mapping.eventCursor, run.task ?? ""));
|
|
20729
|
-
} else {
|
|
20730
|
-
runs.transition(run.id, "failed", "server_restarted");
|
|
20731
|
-
store.updateConversationStatus(conversation.id, run.id, "interrupted", { reason: "server_restart_recovery" });
|
|
20732
|
-
}
|
|
20733
19711
|
}
|
|
20734
19712
|
}
|
|
20735
19713
|
} finally {
|
|
@@ -20738,149 +19716,50 @@ var init_aster_api = __esm({
|
|
|
20738
19716
|
})();
|
|
20739
19717
|
return this.recovery;
|
|
20740
19718
|
}
|
|
20741
|
-
|
|
20742
|
-
|
|
20743
|
-
const
|
|
20744
|
-
|
|
20745
|
-
|
|
20746
|
-
|
|
20747
|
-
|
|
20748
|
-
|
|
20749
|
-
|
|
20750
|
-
|
|
20751
|
-
userText,
|
|
20752
|
-
sessionFile: {},
|
|
20753
|
-
messages: [],
|
|
20754
|
-
executor: new ToolExecutor([]),
|
|
20755
|
-
allowedTools: /* @__PURE__ */ new Set(),
|
|
20756
|
-
maxToolIterations: null,
|
|
20757
|
-
maxRuntimeMs: null,
|
|
20758
|
-
askPermission: async () => "deny",
|
|
20759
|
-
publishEvent,
|
|
20760
|
-
onCellCursor: (next) => store.advanceCellEventCursor(conversation.id, next),
|
|
20761
|
-
finish: (status, reason) => {
|
|
20762
|
-
const runs = new RunStore();
|
|
20763
|
-
try {
|
|
20764
|
-
const run = runs.getRun(runId);
|
|
20765
|
-
if (run && ["queued", "running", "waiting", "needs_input"].includes(run.status)) runs.transition(runId, status, reason ?? status);
|
|
20766
|
-
} catch {
|
|
20767
|
-
} finally {
|
|
20768
|
-
runs.close();
|
|
20769
|
-
}
|
|
20770
|
-
const updated = store.updateConversationStatus(conversation.id, runId, status, reason ? { reason } : {});
|
|
20771
|
-
if (updated) store.appendEvent(conversation.id, runId, status, reason ? { reason } : {});
|
|
20772
|
-
if (updated) this.notifyTurnEnd(store, conversation.id, runId, status, reason, userText, 0);
|
|
20773
|
-
}
|
|
20774
|
-
};
|
|
20775
|
-
}
|
|
20776
|
-
async createConversation(res, store, principal, workspace, model, idempotencyKey, kind, repository) {
|
|
20777
|
-
const candidateId = randomUUID8();
|
|
20778
|
-
const candidateSessionId = randomUUID8();
|
|
20779
|
-
const reservation = store.reserveCellConversation({
|
|
20780
|
-
conversationId: candidateId,
|
|
20781
|
-
credentialId: principal.credentialId,
|
|
20782
|
-
workspaceId: workspace.id,
|
|
20783
|
-
model,
|
|
20784
|
-
sessionId: candidateSessionId,
|
|
20785
|
-
cellName: `aster-${candidateId}`,
|
|
20786
|
-
idempotencyKey
|
|
20787
|
-
});
|
|
20788
|
-
if (reservation.kind === "conflict") return sendError(res, 409, "idempotency_conflict", "Idempotency-Key was already used for different conversation parameters");
|
|
20789
|
-
const mapping = reservation.mapping;
|
|
20790
|
-
if (mapping.status === "destroyed") return sendError(res, 410, "conversation_destroyed", "Conversation was already destroyed");
|
|
20791
|
-
if (mapping.status === "ready") {
|
|
20792
|
-
const existing = store.getConversation(mapping.conversationId);
|
|
20793
|
-
if (existing) return json(res, 200, publicConversation(existing, store.getConversationProject(existing.id)));
|
|
20794
|
-
}
|
|
20795
|
-
if (!this.runtime.provisionConversation) {
|
|
20796
|
-
store.setCellStatus(mapping.conversationId, "failed");
|
|
20797
|
-
return sendError(res, 503, "hotcell_unavailable", "Hotcell conversation runtime is not configured");
|
|
20798
|
-
}
|
|
20799
|
-
let project;
|
|
20800
|
-
if (kind) {
|
|
20801
|
-
let stored = store.getConversationProject(mapping.conversationId);
|
|
20802
|
-
if (!stored) {
|
|
20803
|
-
if (kind === "code") {
|
|
20804
|
-
let repo;
|
|
20805
|
-
try {
|
|
20806
|
-
repo = await this.github.getRepository(repository);
|
|
20807
|
-
} catch (error) {
|
|
20808
|
-
if (error instanceof AsterGitHubError) return sendError(res, error.status, error.code, error.message);
|
|
20809
|
-
throw error;
|
|
20810
|
-
}
|
|
20811
|
-
store.setConversationProject(mapping.conversationId, {
|
|
20812
|
-
kind,
|
|
20813
|
-
repository: repo.fullName,
|
|
20814
|
-
branch: `aster/${mapping.conversationId.slice(0, 8)}`
|
|
20815
|
-
});
|
|
20816
|
-
stored = { kind, repository: repo.fullName, branch: `aster/${mapping.conversationId.slice(0, 8)}` };
|
|
20817
|
-
project = { kind, repository: repo.fullName, baseBranch: repo.defaultBranch, branch: stored.branch };
|
|
20818
|
-
} else {
|
|
20819
|
-
store.setConversationProject(mapping.conversationId, { kind, repository: null, branch: null });
|
|
20820
|
-
stored = { kind, repository: null, branch: null };
|
|
20821
|
-
}
|
|
20822
|
-
}
|
|
20823
|
-
if (!project) {
|
|
20824
|
-
project = stored.kind === "code" ? { kind: "code", repository: stored.repository, baseBranch: (await this.github.getRepository(stored.repository).catch(() => void 0))?.defaultBranch ?? "main", branch: stored.branch } : { kind: "chat" };
|
|
20825
|
-
}
|
|
20826
|
-
if (project.kind === "code" && project.repository && this.githubProxyUrl) {
|
|
20827
|
-
project.github = {
|
|
20828
|
-
token: store.getOrCreateGitHubGrant(mapping.conversationId, project.repository),
|
|
20829
|
-
gitUrl: `${this.githubProxyUrl}/git`,
|
|
20830
|
-
apiUrl: `${this.githubProxyUrl}/api`
|
|
20831
|
-
};
|
|
20832
|
-
}
|
|
19719
|
+
async createConversation(res, store, workspace, model) {
|
|
19720
|
+
const id = randomUUID7();
|
|
19721
|
+
const sessionId = randomUUID7();
|
|
19722
|
+
const worktreeManager = new RunWorktreeManager();
|
|
19723
|
+
let worktree;
|
|
19724
|
+
try {
|
|
19725
|
+
worktree = await worktreeManager.create(id, workspace.rootPath);
|
|
19726
|
+
} catch {
|
|
19727
|
+
sendError(res, 409, "workspace_repository_unavailable", "Configured workspace must be an accessible Git repository");
|
|
19728
|
+
return;
|
|
20833
19729
|
}
|
|
19730
|
+
const tools = createAsterTools(worktree.cwd);
|
|
19731
|
+
const messages = [
|
|
19732
|
+
{ role: "system", content: buildSystemPrompt({ cwd: worktree.cwd, tools, model, preferPullRequests: true }) },
|
|
19733
|
+
{ role: "system", content: "You are operating through Aster. Only read, write, and edit tools are available. All file access is confined to the selected workspace. Never request, reveal, store, or repeat provider credentials or secrets." }
|
|
19734
|
+
];
|
|
19735
|
+
const sessionFile = {
|
|
19736
|
+
id: sessionId,
|
|
19737
|
+
cwd: worktree.cwd,
|
|
19738
|
+
model,
|
|
19739
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19740
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19741
|
+
messages,
|
|
19742
|
+
title: "Aster conversation"
|
|
19743
|
+
};
|
|
20834
19744
|
try {
|
|
20835
|
-
|
|
20836
|
-
|
|
20837
|
-
|
|
20838
|
-
conversationId: mapping.conversationId,
|
|
20839
|
-
sessionId: mapping.sessionId,
|
|
20840
|
-
cellName: mapping.cellName,
|
|
20841
|
-
workspaceId: workspace.id,
|
|
20842
|
-
workspaceRoot: workspace.rootPath,
|
|
20843
|
-
model,
|
|
20844
|
-
allowCreate,
|
|
20845
|
-
onCellCreated: (cellId) => store.setCellId(mapping.conversationId, cellId),
|
|
20846
|
-
...project ? { project } : {}
|
|
20847
|
-
});
|
|
20848
|
-
store.setCellId(mapping.conversationId, provisioned.cellId);
|
|
20849
|
-
const conversation = store.getConversation(mapping.conversationId) ?? store.createConversation({
|
|
20850
|
-
id: mapping.conversationId,
|
|
19745
|
+
await saveSession(sessionFile);
|
|
19746
|
+
const conversation = store.createConversation({
|
|
19747
|
+
id,
|
|
20851
19748
|
workspaceId: workspace.id,
|
|
20852
19749
|
model,
|
|
20853
|
-
sessionId
|
|
20854
|
-
worktreePath:
|
|
20855
|
-
cwd:
|
|
20856
|
-
branch:
|
|
19750
|
+
sessionId,
|
|
19751
|
+
worktreePath: worktree.worktreePath,
|
|
19752
|
+
cwd: worktree.cwd,
|
|
19753
|
+
branch: worktree.branch
|
|
20857
19754
|
});
|
|
20858
|
-
|
|
20859
|
-
|
|
20860
|
-
cwd: "/workspace",
|
|
20861
|
-
model,
|
|
20862
|
-
createdAt: new Date(mapping.createdAt).toISOString(),
|
|
20863
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
20864
|
-
messages: [],
|
|
20865
|
-
title: "Aster conversation"
|
|
20866
|
-
};
|
|
20867
|
-
await saveSession(sessionFile);
|
|
20868
|
-
store.setCellStatus(mapping.conversationId, "ready");
|
|
20869
|
-
store.appendEvent(mapping.conversationId, null, "conversation.created", { conversationId: mapping.conversationId, workspaceId: workspace.id, model });
|
|
20870
|
-
json(res, reservation.kind === "created" ? 201 : 200, publicConversation(conversation, store.getConversationProject(conversation.id)));
|
|
19755
|
+
store.appendEvent(id, null, "conversation.created", { conversationId: id, workspaceId: workspace.id, model });
|
|
19756
|
+
json(res, 201, publicConversation(conversation));
|
|
20871
19757
|
} catch {
|
|
20872
|
-
|
|
20873
|
-
|
|
20874
|
-
|
|
20875
|
-
|
|
20876
|
-
|
|
20877
|
-
} catch {
|
|
20878
|
-
store.setCellStatus(mapping.conversationId, "cleanup_pending");
|
|
20879
|
-
}
|
|
20880
|
-
} else {
|
|
20881
|
-
store.setCellStatus(mapping.conversationId, "failed");
|
|
20882
|
-
}
|
|
20883
|
-
sendError(res, 503, "conversation_provisioning_failed", "Conversation provisioning failed; retry with the same Idempotency-Key to reconcile its state");
|
|
19758
|
+
await unlink2(resolve13(sessionsDir(), `${sessionId}.json`)).catch(() => {
|
|
19759
|
+
});
|
|
19760
|
+
await worktreeManager.discard(worktree).catch(() => {
|
|
19761
|
+
});
|
|
19762
|
+
sendError(res, 500, "conversation_create_failed", "Conversation could not be created");
|
|
20884
19763
|
}
|
|
20885
19764
|
}
|
|
20886
19765
|
async appendTurn(res, store, conversation, clientTurnId, requestText, text) {
|
|
@@ -20896,16 +19775,7 @@ var init_aster_api = __esm({
|
|
|
20896
19775
|
return;
|
|
20897
19776
|
}
|
|
20898
19777
|
}
|
|
20899
|
-
const
|
|
20900
|
-
if (cellMapping?.status === "paused") {
|
|
20901
|
-
sendError(res, 409, "conversation_paused", "Conversation is paused; resume it before submitting a turn");
|
|
20902
|
-
return;
|
|
20903
|
-
}
|
|
20904
|
-
if (!cellMapping?.cellId || ["provisioning", "destroying", "cleanup_pending", "destroyed", "failed"].includes(cellMapping.status)) {
|
|
20905
|
-
sendError(res, 409, "conversation_cell_unavailable", "Conversation cell is not available");
|
|
20906
|
-
return;
|
|
20907
|
-
}
|
|
20908
|
-
const runId = randomUUID8();
|
|
19778
|
+
const runId = randomUUID7();
|
|
20909
19779
|
if (!idempotent && !store.beginTurn(conversation.id, runId)) {
|
|
20910
19780
|
sendError(res, 409, "conversation_busy", "A turn is already active for this conversation");
|
|
20911
19781
|
return;
|
|
@@ -20982,13 +19852,9 @@ var init_aster_api = __esm({
|
|
|
20982
19852
|
const publishEvent = (type, data) => {
|
|
20983
19853
|
store.appendEvent(conversation.id, runId, type, data);
|
|
20984
19854
|
};
|
|
20985
|
-
const turnStartSequence = store.getConversation(conversation.id)?.lastEventSequence ?? 0;
|
|
20986
19855
|
const active = {
|
|
20987
19856
|
runId,
|
|
20988
19857
|
conversation,
|
|
20989
|
-
cellId: cellMapping.cellId,
|
|
20990
|
-
cellEventCursor: cellMapping.eventCursor,
|
|
20991
|
-
userText: text,
|
|
20992
19858
|
sessionFile,
|
|
20993
19859
|
messages: sessionFile.messages,
|
|
20994
19860
|
executor,
|
|
@@ -20997,24 +19863,11 @@ var init_aster_api = __esm({
|
|
|
20997
19863
|
maxRuntimeMs: run.maxRuntimeMs,
|
|
20998
19864
|
askPermission: (request) => this.askForApproval(store, conversation, runId, request, publishEvent),
|
|
20999
19865
|
publishEvent,
|
|
21000
|
-
onCellCursor: (cursor) => store.advanceCellEventCursor(conversation.id, cursor),
|
|
21001
19866
|
finish: (status, reason) => {
|
|
21002
19867
|
const updated = store.updateConversationStatus(conversation.id, runId, status, reason ? { reason } : {});
|
|
21003
19868
|
if (updated) store.appendEvent(conversation.id, runId, status, reason ? { reason } : {});
|
|
21004
|
-
if (updated) this.notifyTurnEnd(store, conversation.id, runId, status, reason, text, turnStartSequence);
|
|
21005
19869
|
}
|
|
21006
19870
|
};
|
|
21007
|
-
const memory = this.getMemory();
|
|
21008
|
-
if (memory) {
|
|
21009
|
-
active.memory = { note: memory.brief(conversation.id, text), snapshot: memory.snapshot() };
|
|
21010
|
-
const firstSequence = store.getConversation(conversation.id)?.lastEventSequence ?? 0;
|
|
21011
|
-
active.beforeFinish = async () => {
|
|
21012
|
-
const reply = collectAssistantText(store, conversation.id, runId, firstSequence);
|
|
21013
|
-
const project = store.getConversationProject(conversation.id);
|
|
21014
|
-
const changes = await memory.remember({ conversationId: conversation.id, project, userText: text, assistantText: reply });
|
|
21015
|
-
if (changes.length) publishEvent("memory.updated", { items: changes });
|
|
21016
|
-
};
|
|
21017
|
-
}
|
|
21018
19871
|
this.runtime.startTurn(active);
|
|
21019
19872
|
json(res, 202, acceptedResponse);
|
|
21020
19873
|
} catch {
|
|
@@ -21044,8 +19897,9 @@ var init_aster_api = __esm({
|
|
|
21044
19897
|
publish("tool.activity", { tool: request.tool.name, activity: "rejected", reason: "unsafe_payload" });
|
|
21045
19898
|
return "deny";
|
|
21046
19899
|
}
|
|
19900
|
+
const workspaceRoot = conversation.cwd;
|
|
21047
19901
|
try {
|
|
21048
|
-
|
|
19902
|
+
await assertAsterWorkspacePath(workspaceRoot, request.args.path, request.tool.name === "write");
|
|
21049
19903
|
} catch {
|
|
21050
19904
|
publish("tool.activity", { tool: request.tool.name, activity: "rejected", reason: "workspace_path_forbidden" });
|
|
21051
19905
|
return "deny";
|
|
@@ -21070,16 +19924,6 @@ var init_aster_api = __esm({
|
|
|
21070
19924
|
}
|
|
21071
19925
|
store.updateConversationStatus(conversation.id, runId, "waiting_approval", { approvalId: approval.id });
|
|
21072
19926
|
publish("approval.required", { approvalId: approval.id, tool: approval.toolName, explanation: approval.explanation, expiresAt: approval.expiresAt });
|
|
21073
|
-
const push = this.getPush();
|
|
21074
|
-
if (push?.configured) {
|
|
21075
|
-
void push.notify({
|
|
21076
|
-
title: "Approval required",
|
|
21077
|
-
body: approval.toolName + ": " + approval.explanation,
|
|
21078
|
-
conversationId: conversation.id,
|
|
21079
|
-
category: "APPROVAL",
|
|
21080
|
-
approvalId: approval.id
|
|
21081
|
-
});
|
|
21082
|
-
}
|
|
21083
19927
|
return new Promise((resolveDecision) => {
|
|
21084
19928
|
const timer3 = setTimeout(() => {
|
|
21085
19929
|
void this.expireApproval(store, approval.id);
|
|
@@ -21136,115 +19980,6 @@ var init_aster_api = __esm({
|
|
|
21136
19980
|
waiter.resolve(status === "expired" ? "deny" : decision);
|
|
21137
19981
|
}
|
|
21138
19982
|
}
|
|
21139
|
-
async pauseConversation(res, store, conversation) {
|
|
21140
|
-
const mapping = store.getCellMapping(conversation.id);
|
|
21141
|
-
if (!mapping || !mapping.cellId) return sendError(res, 409, "cell_not_ready", "Conversation cell is not ready");
|
|
21142
|
-
if (mapping.status === "paused") return json(res, 200, { ...publicConversation(conversation), cellStatus: "paused" });
|
|
21143
|
-
if (conversation.activeRunId) return sendError(res, 409, "turn_active", "Pause is only available at a safe boundary; cancel or wait for the active turn first");
|
|
21144
|
-
if (!this.runtime.pauseCell) return sendError(res, 503, "hotcell_pause_unavailable", "Hotcell pause is not configured");
|
|
21145
|
-
try {
|
|
21146
|
-
await this.runtime.pauseCell(mapping.cellId);
|
|
21147
|
-
store.setCellStatus(conversation.id, "paused");
|
|
21148
|
-
store.setConversationStatus(conversation.id, "paused", { reason: "safe_boundary" });
|
|
21149
|
-
store.appendEvent(conversation.id, null, "conversation.paused", { strategy: "safe_boundary" });
|
|
21150
|
-
const updated = store.getConversation(conversation.id);
|
|
21151
|
-
json(res, 200, { ...publicConversation(updated), cellStatus: "paused" });
|
|
21152
|
-
} catch {
|
|
21153
|
-
sendError(res, 503, "pause_failed", "Conversation could not be paused; its lifecycle state was not changed");
|
|
21154
|
-
}
|
|
21155
|
-
}
|
|
21156
|
-
async resumeConversation(res, store, conversation) {
|
|
21157
|
-
const mapping = store.getCellMapping(conversation.id);
|
|
21158
|
-
if (!mapping || !mapping.cellId) return sendError(res, 409, "cell_not_ready", "Conversation cell is not ready");
|
|
21159
|
-
if (mapping.status !== "paused") return json(res, 200, { ...publicConversation(conversation), cellStatus: mapping.status });
|
|
21160
|
-
if (!this.runtime.resumeCell) return sendError(res, 503, "hotcell_resume_unavailable", "Hotcell resume is not configured");
|
|
21161
|
-
try {
|
|
21162
|
-
await this.runtime.resumeCell(mapping.cellId);
|
|
21163
|
-
store.setCellStatus(conversation.id, "ready");
|
|
21164
|
-
store.setConversationStatus(conversation.id, "ready", { reason: "resumed" });
|
|
21165
|
-
store.appendEvent(conversation.id, null, "conversation.resumed", { sessionId: mapping.sessionId });
|
|
21166
|
-
const updated = store.getConversation(conversation.id);
|
|
21167
|
-
json(res, 200, { ...publicConversation(updated), cellStatus: "ready" });
|
|
21168
|
-
} catch {
|
|
21169
|
-
sendError(res, 503, "resume_failed", "Conversation could not be resumed; the persisted cell state is unchanged");
|
|
21170
|
-
}
|
|
21171
|
-
}
|
|
21172
|
-
async destroyConversation(res, store, conversation) {
|
|
21173
|
-
const mapping = store.getCellMapping(conversation.id);
|
|
21174
|
-
if (!mapping) return sendError(res, 404, "conversation_not_found", "Conversation not found");
|
|
21175
|
-
if (mapping.status === "destroyed") return json(res, 200, { conversationId: conversation.id, status: "destroyed" });
|
|
21176
|
-
if (!this.runtime.destroyCell) return sendError(res, 503, "hotcell_destroy_unavailable", "Hotcell cleanup is not configured");
|
|
21177
|
-
store.setCellStatus(conversation.id, "destroying");
|
|
21178
|
-
store.revokeGitHubGrant(conversation.id);
|
|
21179
|
-
if (conversation.activeRunId) {
|
|
21180
|
-
for (const approval of store.cancelPendingApprovals(conversation.id)) {
|
|
21181
|
-
const waiter = this.waiters.get(approval.id);
|
|
21182
|
-
if (waiter) {
|
|
21183
|
-
clearTimeout(waiter.timer);
|
|
21184
|
-
this.waiters.delete(approval.id);
|
|
21185
|
-
waiter.resolve("deny");
|
|
21186
|
-
}
|
|
21187
|
-
}
|
|
21188
|
-
this.runtime.cancelRun(conversation.activeRunId);
|
|
21189
|
-
store.updateConversationStatus(conversation.id, conversation.activeRunId, "cancelled", { reason: "conversation_destroyed" });
|
|
21190
|
-
}
|
|
21191
|
-
try {
|
|
21192
|
-
if (mapping.cellId) await this.runtime.destroyCell(mapping.cellId);
|
|
21193
|
-
await unlink2(resolve13(sessionsDir(), mapping.sessionId + ".json")).catch(() => {
|
|
21194
|
-
});
|
|
21195
|
-
store.setCellStatus(conversation.id, "destroyed");
|
|
21196
|
-
store.setConversationStatus(conversation.id, "destroyed");
|
|
21197
|
-
store.appendEvent(conversation.id, null, "conversation.destroyed", { conversationId: conversation.id });
|
|
21198
|
-
json(res, 200, { conversationId: conversation.id, status: "destroyed" });
|
|
21199
|
-
} catch {
|
|
21200
|
-
store.setCellStatus(conversation.id, "cleanup_pending");
|
|
21201
|
-
sendError(res, 503, "cleanup_pending", "Cell cleanup is pending; retry DELETE to revoke credentials and remove the cell");
|
|
21202
|
-
}
|
|
21203
|
-
}
|
|
21204
|
-
/** The conversation's cell id when it can take control requests; otherwise sends the error. */
|
|
21205
|
-
readyCell(res, store, conversationId) {
|
|
21206
|
-
const mapping = store.getCellMapping(conversationId);
|
|
21207
|
-
if (mapping?.status === "paused") {
|
|
21208
|
-
sendError(res, 409, "conversation_paused", "Conversation is paused; resume it first");
|
|
21209
|
-
return void 0;
|
|
21210
|
-
}
|
|
21211
|
-
if (!mapping?.cellId || mapping.status !== "ready") {
|
|
21212
|
-
sendError(res, 409, "conversation_cell_unavailable", "Conversation cell is not available");
|
|
21213
|
-
return void 0;
|
|
21214
|
-
}
|
|
21215
|
-
return mapping.cellId;
|
|
21216
|
-
}
|
|
21217
|
-
/** Switch the cell session first, then the record, so a failed switch leaves both on the old model. */
|
|
21218
|
-
async switchModel(res, store, conversation, model) {
|
|
21219
|
-
if (conversation.activeRunId) {
|
|
21220
|
-
sendError(res, 409, "conversation_busy", "A turn is already active for this conversation");
|
|
21221
|
-
return void 0;
|
|
21222
|
-
}
|
|
21223
|
-
const cell = this.readyCell(res, store, conversation.id);
|
|
21224
|
-
if (!cell) return void 0;
|
|
21225
|
-
if (!this.runtime.setModel) {
|
|
21226
|
-
sendError(res, 503, "model_switch_unavailable", "Model switching is not configured");
|
|
21227
|
-
return void 0;
|
|
21228
|
-
}
|
|
21229
|
-
try {
|
|
21230
|
-
await this.runtime.setModel(cell, model);
|
|
21231
|
-
} catch {
|
|
21232
|
-
sendError(res, 503, "model_switch_failed", "The conversation's model could not be switched");
|
|
21233
|
-
return void 0;
|
|
21234
|
-
}
|
|
21235
|
-
if (!store.setConversationModel(conversation.id, model)) {
|
|
21236
|
-
sendError(res, 409, "conversation_busy", "A turn is already active for this conversation");
|
|
21237
|
-
return void 0;
|
|
21238
|
-
}
|
|
21239
|
-
try {
|
|
21240
|
-
const sessionPath = resolve13(sessionsDir(), conversation.sessionId + ".json");
|
|
21241
|
-
const sessionFile = await loadSession(sessionPath);
|
|
21242
|
-
sessionFile.model = model;
|
|
21243
|
-
await saveSession(sessionFile);
|
|
21244
|
-
} catch {
|
|
21245
|
-
}
|
|
21246
|
-
return store.getConversation(conversation.id);
|
|
21247
|
-
}
|
|
21248
19983
|
async cancelConversation(res, store, conversation) {
|
|
21249
19984
|
const runId = conversation.activeRunId;
|
|
21250
19985
|
if (!runId) {
|
|
@@ -21287,16 +20022,6 @@ var init_aster_api = __esm({
|
|
|
21287
20022
|
let last = cursor;
|
|
21288
20023
|
let closed = false;
|
|
21289
20024
|
let polling = false;
|
|
21290
|
-
let released = false;
|
|
21291
|
-
const connected = this.connectedEventStreams.get(conversationId) ?? 0;
|
|
21292
|
-
this.connectedEventStreams.set(conversationId, connected + 1);
|
|
21293
|
-
const release2 = () => {
|
|
21294
|
-
if (released) return;
|
|
21295
|
-
released = true;
|
|
21296
|
-
const remaining = (this.connectedEventStreams.get(conversationId) ?? 1) - 1;
|
|
21297
|
-
if (remaining > 0) this.connectedEventStreams.set(conversationId, remaining);
|
|
21298
|
-
else this.connectedEventStreams.delete(conversationId);
|
|
21299
|
-
};
|
|
21300
20025
|
let lastHeartbeat = Date.now();
|
|
21301
20026
|
const pump = async () => {
|
|
21302
20027
|
if (closed || polling) return;
|
|
@@ -21318,7 +20043,6 @@ data: ${JSON.stringify(event.data)}
|
|
|
21318
20043
|
}
|
|
21319
20044
|
} catch {
|
|
21320
20045
|
closed = true;
|
|
21321
|
-
release2();
|
|
21322
20046
|
res.end();
|
|
21323
20047
|
} finally {
|
|
21324
20048
|
polling = false;
|
|
@@ -21329,9 +20053,7 @@ data: ${JSON.stringify(event.data)}
|
|
|
21329
20053
|
};
|
|
21330
20054
|
res.on("close", () => {
|
|
21331
20055
|
closed = true;
|
|
21332
|
-
release2();
|
|
21333
20056
|
});
|
|
21334
|
-
res.on("finish", release2);
|
|
21335
20057
|
void pump();
|
|
21336
20058
|
}
|
|
21337
20059
|
async recover(store) {
|
|
@@ -21368,8 +20090,7 @@ data: ${JSON.stringify(event.data)}
|
|
|
21368
20090
|
async requireConversation(store, principal, id) {
|
|
21369
20091
|
if (!CONVERSATION_ID_RE.test(id)) return void 0;
|
|
21370
20092
|
const conversation = store.getConversation(id);
|
|
21371
|
-
|
|
21372
|
-
return conversation && mapping?.credentialId === principal.credentialId && principal.workspaceIds.includes(conversation.workspaceId) ? conversation : void 0;
|
|
20093
|
+
return conversation && principal.workspaceIds.includes(conversation.workspaceId) ? conversation : void 0;
|
|
21373
20094
|
}
|
|
21374
20095
|
};
|
|
21375
20096
|
}
|
|
@@ -21379,7 +20100,7 @@ data: ${JSON.stringify(event.data)}
|
|
|
21379
20100
|
import { execFile as execFile8 } from "child_process";
|
|
21380
20101
|
import { readFile as readFile26 } from "fs/promises";
|
|
21381
20102
|
import { promisify as promisify7 } from "util";
|
|
21382
|
-
import { randomUUID as
|
|
20103
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
21383
20104
|
async function writeCellFileChunked(cell, path, content) {
|
|
21384
20105
|
if (Buffer.byteLength(content, "utf8") <= CELL_FILE_WRITE_CHUNK_CHARS) {
|
|
21385
20106
|
await cell.writeFile(path, content, { mode: "0600" });
|
|
@@ -21575,7 +20296,7 @@ var init_aster_cell_runtime = __esm({
|
|
|
21575
20296
|
await this.installRuntime(cell);
|
|
21576
20297
|
await this.ensureBridge(cell);
|
|
21577
20298
|
await this.control(cell, "POST", "/rpc", {
|
|
21578
|
-
id:
|
|
20299
|
+
id: randomUUID8(),
|
|
21579
20300
|
type: "new_session",
|
|
21580
20301
|
sessionId: input.sessionId,
|
|
21581
20302
|
config: { model: input.model, cwd: CELL_WORKSPACE }
|
|
@@ -21594,7 +20315,7 @@ var init_aster_cell_runtime = __esm({
|
|
|
21594
20315
|
/** Switch the cell session's model between turns. The bridge persists it for session restore. */
|
|
21595
20316
|
async setModel(cellId, model) {
|
|
21596
20317
|
await this.ensureReady(cellId);
|
|
21597
|
-
await this.controlById(cellId, "POST", "/rpc", { id:
|
|
20318
|
+
await this.controlById(cellId, "POST", "/rpc", { id: randomUUID8(), type: "set_model", modelId: model });
|
|
21598
20319
|
}
|
|
21599
20320
|
/** Reinstall the runtime if the cell's image lost it and restart the bridge if it isn't running. */
|
|
21600
20321
|
async ensureReady(cellId) {
|
|
@@ -21657,7 +20378,7 @@ var init_aster_cell_runtime = __esm({
|
|
|
21657
20378
|
const active = this.activeRuns.get(runId);
|
|
21658
20379
|
if (!active) return;
|
|
21659
20380
|
active.stop();
|
|
21660
|
-
void this.controlById(active.cellId, "POST", "/rpc", { id:
|
|
20381
|
+
void this.controlById(active.cellId, "POST", "/rpc", { id: randomUUID8(), type: "abort" }).catch(() => {
|
|
21661
20382
|
});
|
|
21662
20383
|
}
|
|
21663
20384
|
async runTurn(turn, options = {}) {
|
|
@@ -21757,7 +20478,7 @@ ${turn.userText}` : turn.userText;
|
|
|
21757
20478
|
args: event.args ?? {}
|
|
21758
20479
|
});
|
|
21759
20480
|
await this.controlById(cellId, "POST", "/rpc", {
|
|
21760
|
-
id:
|
|
20481
|
+
id: randomUUID8(),
|
|
21761
20482
|
type: "resolve_permission",
|
|
21762
20483
|
requestId: event.requestId,
|
|
21763
20484
|
decision
|
|
@@ -21984,7 +20705,7 @@ ${text}` : text, { mode: "0600" });
|
|
|
21984
20705
|
}
|
|
21985
20706
|
}
|
|
21986
20707
|
async control(cell, method, path, body) {
|
|
21987
|
-
const requestId =
|
|
20708
|
+
const requestId = randomUUID8();
|
|
21988
20709
|
await cell.mkdir(CELL_CONTROL_DIR, { parents: true });
|
|
21989
20710
|
await writeCellFileChunked(cell, `${CELL_CONTROL_DIR}/${requestId}.json`, JSON.stringify({ method, path, body }));
|
|
21990
20711
|
const result2 = await cell.exec(`${CONTROL_RUNNER} ${requestId}`);
|
|
@@ -22200,7 +20921,7 @@ var init_hotcell_provider = __esm({
|
|
|
22200
20921
|
});
|
|
22201
20922
|
|
|
22202
20923
|
// src/server/routes.ts
|
|
22203
|
-
import { randomUUID as
|
|
20924
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
22204
20925
|
import { URL as URL2 } from "url";
|
|
22205
20926
|
import { readFile as readFile27, unlink as unlink3 } from "fs/promises";
|
|
22206
20927
|
import { resolve as resolve14, basename as basename7 } from "path";
|
|
@@ -22544,7 +21265,7 @@ function setupRoutes(config2, options = {}) {
|
|
|
22544
21265
|
const executor = new ToolExecutor(tools);
|
|
22545
21266
|
const { makeSessionId: makeSessionId2 } = await Promise.resolve().then(() => (init_sessions(), sessions_exports));
|
|
22546
21267
|
const sessionId = makeSessionId2(task);
|
|
22547
|
-
const runId =
|
|
21268
|
+
const runId = randomUUID9();
|
|
22548
21269
|
const worktreeManager = new RunWorktreeManager();
|
|
22549
21270
|
let worktree;
|
|
22550
21271
|
if (useWorktree) {
|
|
@@ -23153,10 +21874,10 @@ var server_exports = {};
|
|
|
23153
21874
|
__export(server_exports, {
|
|
23154
21875
|
startServer: () => startServer
|
|
23155
21876
|
});
|
|
23156
|
-
import { createServer
|
|
21877
|
+
import { createServer } from "http";
|
|
23157
21878
|
async function startServer(opts2) {
|
|
23158
21879
|
const { handleRequest, cleanup } = setupRoutes(opts2.config, { asterRuntime: opts2.asterRuntime });
|
|
23159
|
-
const server =
|
|
21880
|
+
const server = createServer((req, res) => {
|
|
23160
21881
|
const isAsterApi = req.url?.startsWith("/api/v1/") === true;
|
|
23161
21882
|
if (!isAsterApi) {
|
|
23162
21883
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
@@ -24642,7 +23363,7 @@ __export(app_helpers_exports, {
|
|
|
24642
23363
|
trackRecentFile: () => trackRecentFile
|
|
24643
23364
|
});
|
|
24644
23365
|
import { execSync as execSync3, spawn as spawn7 } from "child_process";
|
|
24645
|
-
import { existsSync as existsSync8, readFileSync as
|
|
23366
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6, statSync as statSync5 } from "fs";
|
|
24646
23367
|
import { join as join41 } from "path";
|
|
24647
23368
|
import { platform as platform3 } from "os";
|
|
24648
23369
|
function replaceQueuedPrompt(queue2, key, full, display) {
|
|
@@ -24756,7 +23477,7 @@ function buildFilePickerIgnoreList(cwd) {
|
|
|
24756
23477
|
if (stats.size > MAX_GITIGNORE_SIZE) {
|
|
24757
23478
|
return hardcoded;
|
|
24758
23479
|
}
|
|
24759
|
-
const content =
|
|
23480
|
+
const content = readFileSync6(gitignorePath, "utf-8");
|
|
24760
23481
|
for (const line of content.split(/\r?\n/)) {
|
|
24761
23482
|
const trimmed = line.trim();
|
|
24762
23483
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -34315,7 +33036,7 @@ __export(openrouter_oauth_exports, {
|
|
|
34315
33036
|
startLoopbackListener: () => startLoopbackListener
|
|
34316
33037
|
});
|
|
34317
33038
|
import { createHash as createHash6, randomBytes as randomBytes3 } from "crypto";
|
|
34318
|
-
import { createServer as
|
|
33039
|
+
import { createServer as createServer2 } from "http";
|
|
34319
33040
|
import { hostname } from "os";
|
|
34320
33041
|
function createPkce() {
|
|
34321
33042
|
const verifier = randomBytes3(32).toString("base64url");
|
|
@@ -34371,7 +33092,7 @@ async function startLoopbackListener(opts2 = {}) {
|
|
|
34371
33092
|
});
|
|
34372
33093
|
code.catch(() => void 0);
|
|
34373
33094
|
let settled = false;
|
|
34374
|
-
const server =
|
|
33095
|
+
const server = createServer2((req, res) => {
|
|
34375
33096
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
34376
33097
|
if (url.pathname !== "/callback") {
|
|
34377
33098
|
res.writeHead(404).end();
|
|
@@ -39775,7 +38496,7 @@ var init_run_compact = __esm({
|
|
|
39775
38496
|
|
|
39776
38497
|
// src/commands/save.ts
|
|
39777
38498
|
import { mkdir as mkdir18, writeFile as writeFile16, unlink as unlink5 } from "fs/promises";
|
|
39778
|
-
import { dirname as
|
|
38499
|
+
import { dirname as dirname21 } from "path";
|
|
39779
38500
|
async function saveCustomCommand(opts2) {
|
|
39780
38501
|
const dir = opts2.source === "project" ? projectCommandsDir(opts2.cwd) : globalCommandsDir();
|
|
39781
38502
|
const filepath = `${dir}/${opts2.name}.md`;
|
|
@@ -39786,7 +38507,7 @@ async function saveCustomCommand(opts2) {
|
|
|
39786
38507
|
if (opts2.effort) data.effort = opts2.effort;
|
|
39787
38508
|
const frontmatter = serializeFrontmatter(data);
|
|
39788
38509
|
const content = frontmatter + opts2.template;
|
|
39789
|
-
await mkdir18(
|
|
38510
|
+
await mkdir18(dirname21(filepath), { recursive: true });
|
|
39790
38511
|
await writeFile16(filepath, content, "utf8");
|
|
39791
38512
|
return { filepath };
|
|
39792
38513
|
}
|
|
@@ -41999,21 +40720,6 @@ ${result2.output ?? "(output not captured)"}`,
|
|
|
41999
40720
|
[processMessage, handleSlash, queue2, history, historyIndex, events]
|
|
42000
40721
|
);
|
|
42001
40722
|
submitRef.current = submit;
|
|
42002
|
-
useEffect11(() => {
|
|
42003
|
-
if (!bridge) return;
|
|
42004
|
-
const seen = announcedSubagentsRef.current;
|
|
42005
|
-
const fresh = runningSubagents.filter((w) => !seen.has(w.id));
|
|
42006
|
-
if (fresh.length === 0) return;
|
|
42007
|
-
for (const w of fresh) seen.add(w.id);
|
|
42008
|
-
setEvents((evts) => [
|
|
42009
|
-
...evts,
|
|
42010
|
-
...fresh.map((w) => ({
|
|
42011
|
-
kind: "info",
|
|
42012
|
-
key: mkKey(),
|
|
42013
|
-
text: `subagent #${w.index} started: ${w.task.replace(/\s+/g, " ").slice(0, 100)} \xB7 /agents to view or stop`
|
|
42014
|
-
}))
|
|
42015
|
-
]);
|
|
42016
|
-
}, [bridge, runningSubagents]);
|
|
42017
40723
|
const textOf = (m) => !m ? "" : typeof m.content === "string" ? m.content : Array.isArray(m.content) ? m.content.filter((p) => p.type === "text").map((p) => p.text).join(" ") : "";
|
|
42018
40724
|
const describeActivity = () => {
|
|
42019
40725
|
const msgs = messagesRef.current;
|