autopilot-ai 1.19.0 → 1.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1614 -205
- package/dist/index.js.map +1 -1
- package/dist/sdk/index.js +32 -7
- package/dist/sdk/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4899,8 +4899,8 @@ var init_subagent_policy = __esm({
|
|
|
4899
4899
|
"src/intent/subagent-policy.ts"() {
|
|
4900
4900
|
"use strict";
|
|
4901
4901
|
init_jev();
|
|
4902
|
-
EXPLICIT_NO = /\b(?:do\s+not|don't|dont|never|avoid|without|no)\s+(?:(?:(?:want\s+to\s+)?(?:use|using|spawn|call|ask|start|launch|skip))\s+)?(?:(?:any|the|a)\s+)?(?:sub[- ]?agents?|agents?|workers?)\b|\b(?:do\s+not|don't|dont|never|avoid|without|no)\s+delegat(?:e|ion|ing)\b|\bwork\s+sequentially\b/i;
|
|
4903
|
-
EXPLICIT_YES = /\b(?:use|spawn|call|start|launch|ask|send|delegate\s+to)\s+(?:(?:some|several|multiple|parallel)\s+)?(?:sub[- ]?agents?|agents?|workers?)\b|\b(?:parallelize|split)\b.{0,50}\b(?:agents?|workers?|subtasks?)\b/i;
|
|
4902
|
+
EXPLICIT_NO = /\b(?:do\s+not|don't|dont|never|avoid|without|no)\s+(?:(?:(?:want\s+to\s+)?(?:use|using|spawn|call|ask|start|launch|skip|run|spin(?:\s+up)?))\s+)?(?:(?:any|the|a|some|several|multiple|parallel|\d+|one|two|three|four|five|six|seven|eight|nine|ten)\s+)?(?:sub[- ]?agents?|agents?|workers?)\b|\b(?:do\s+not|don't|dont|never|avoid|without|no)\s+delegat(?:e|ion|ing)\b|\bwork\s+sequentially\b/i;
|
|
4903
|
+
EXPLICIT_YES = /\b(?:use|spawn|call|start|launch|ask|send|run|spin(?:\s+up)?|delegate\s+to)\s+(?:(?:some|several|multiple|parallel|\d+|one|two|three|four|five|six|seven|eight|nine|ten)\s+)?(?:sub[- ]?agents?|agents?|workers?)\b|\b(?:parallelize|split)\b.{0,50}\b(?:agents?|workers?|subtasks?)\b/i;
|
|
4904
4904
|
SEQUENTIAL_DEPENDENCY = /\b(?:one\s+at\s+a\s+time|sequentially|in\s+sequence|in\s+dependency\s+order|tightly\s+coupled|same\s+(?:file|function|module)|first\b.{0,80}\bbefore\b|before\b.{0,80}\bthen\b|step[- ]by[- ]step)\b/i;
|
|
4905
4905
|
CLEAR_PARALLEL = /\b(?:independent\s+(?:research|tasks?|questions?|work)|in\s+parallel|parallel\s+agents?|separately\s+investigate)\b/i;
|
|
4906
4906
|
AMBIGUOUS_CANDIDATE = /\b(?:research|investigat\w*|audit|compare|contrast|review|comprehensive|across\s+(?:the\s+)?(?:codebase|repo|repository)|entire\s+(?:codebase|repo|repository)|multiple\s+(?:systems|modules|approaches|sources))\b/i;
|
|
@@ -13410,10 +13410,11 @@ async function loadAsterServerConfig(configPath3 = process.env.AUTOPILOT_ASTER_C
|
|
|
13410
13410
|
}
|
|
13411
13411
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new AsterConfigError();
|
|
13412
13412
|
const record = raw;
|
|
13413
|
-
|
|
13413
|
+
const modelCatalog = record.models === "openrouter" ? "openrouter" : "fixed";
|
|
13414
|
+
if (modelCatalog === "fixed" && (!Array.isArray(record.models) || record.models.length === 0 || record.models.length > 100)) {
|
|
13414
13415
|
throw new AsterConfigError();
|
|
13415
13416
|
}
|
|
13416
|
-
const models = [...new Set(record.models)];
|
|
13417
|
+
const models = modelCatalog === "fixed" ? [...new Set(record.models)] : [];
|
|
13417
13418
|
if (models.some((model) => typeof model !== "string" || !model.trim() || model.length > 200)) {
|
|
13418
13419
|
throw new AsterConfigError();
|
|
13419
13420
|
}
|
|
@@ -13451,7 +13452,7 @@ async function loadAsterServerConfig(configPath3 = process.env.AUTOPILOT_ASTER_C
|
|
|
13451
13452
|
roots.push(rootPath);
|
|
13452
13453
|
workspaces.push({ id: workspace.id, displayName: workspace.displayName.trim(), rootPath });
|
|
13453
13454
|
}
|
|
13454
|
-
return { models, workspaces, approvalTtlMs };
|
|
13455
|
+
return { models, modelCatalog, workspaces, approvalTtlMs };
|
|
13455
13456
|
}
|
|
13456
13457
|
function findAsterWorkspace(config2, workspaceId) {
|
|
13457
13458
|
return config2.workspaces.find((workspace) => workspace.id === workspaceId);
|
|
@@ -13476,7 +13477,8 @@ var init_aster_workspaces = __esm({
|
|
|
13476
13477
|
// src/server/aster-store.ts
|
|
13477
13478
|
var aster_store_exports = {};
|
|
13478
13479
|
__export(aster_store_exports, {
|
|
13479
|
-
AsterStore: () => AsterStore
|
|
13480
|
+
AsterStore: () => AsterStore,
|
|
13481
|
+
defaultAsterDbPath: () => defaultAsterDbPath
|
|
13480
13482
|
});
|
|
13481
13483
|
import Database3 from "better-sqlite3";
|
|
13482
13484
|
import { createHash as createHash3, randomBytes as randomBytes2, randomUUID as randomUUID5 } from "crypto";
|
|
@@ -13641,6 +13643,20 @@ var init_aster_store = __esm({
|
|
|
13641
13643
|
UNIQUE(credential_id, create_key_hash)
|
|
13642
13644
|
);
|
|
13643
13645
|
CREATE INDEX IF NOT EXISTS idx_aster_cells_lifecycle ON aster_cell_mappings(status, updated_at);
|
|
13646
|
+
CREATE TABLE IF NOT EXISTS aster_github_grants (
|
|
13647
|
+
conversation_id TEXT PRIMARY KEY,
|
|
13648
|
+
token TEXT NOT NULL UNIQUE,
|
|
13649
|
+
repository TEXT NOT NULL,
|
|
13650
|
+
created_at INTEGER NOT NULL,
|
|
13651
|
+
revoked_at INTEGER
|
|
13652
|
+
);
|
|
13653
|
+
CREATE TABLE IF NOT EXISTS aster_conversation_projects (
|
|
13654
|
+
conversation_id TEXT PRIMARY KEY,
|
|
13655
|
+
kind TEXT NOT NULL,
|
|
13656
|
+
repository TEXT,
|
|
13657
|
+
branch TEXT,
|
|
13658
|
+
created_at INTEGER NOT NULL
|
|
13659
|
+
);
|
|
13644
13660
|
`);
|
|
13645
13661
|
try {
|
|
13646
13662
|
chmodSync2(dbPath, 384);
|
|
@@ -13818,6 +13834,47 @@ var init_aster_store = __esm({
|
|
|
13818
13834
|
});
|
|
13819
13835
|
return update.immediate();
|
|
13820
13836
|
}
|
|
13837
|
+
/** Records a conversation's project once; the first value wins so retries can't change it. */
|
|
13838
|
+
setConversationProject(conversationId, project) {
|
|
13839
|
+
this.db.prepare(`INSERT OR IGNORE INTO aster_conversation_projects (conversation_id, kind, repository, branch, created_at)
|
|
13840
|
+
VALUES (?, ?, ?, ?, ?)`).run(conversationId, project.kind, project.repository, project.branch, Date.now());
|
|
13841
|
+
}
|
|
13842
|
+
getConversationProject(conversationId) {
|
|
13843
|
+
const row = this.db.prepare("SELECT kind, repository, branch FROM aster_conversation_projects WHERE conversation_id = ?").get(conversationId);
|
|
13844
|
+
if (!row) return void 0;
|
|
13845
|
+
return { kind: row.kind === "code" ? "code" : "chat", repository: row.repository, branch: row.branch };
|
|
13846
|
+
}
|
|
13847
|
+
/**
|
|
13848
|
+
* The repository-scoped GitHub token for a code conversation, created once. It is stored
|
|
13849
|
+
* as-is (like the server's own GitHub token, it never leaves the server except into that
|
|
13850
|
+
* conversation's cell) so provisioning retries hand the cell the same value.
|
|
13851
|
+
*/
|
|
13852
|
+
getOrCreateGitHubGrant(conversationId, repository) {
|
|
13853
|
+
const existing = this.db.prepare("SELECT token FROM aster_github_grants WHERE conversation_id = ? AND revoked_at IS NULL").get(conversationId);
|
|
13854
|
+
if (existing) return existing.token;
|
|
13855
|
+
const token = "aghp_" + randomBytes2(32).toString("base64url");
|
|
13856
|
+
this.db.prepare(`INSERT INTO aster_github_grants (conversation_id, token, repository, created_at) VALUES (?, ?, ?, ?)
|
|
13857
|
+
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());
|
|
13858
|
+
return token;
|
|
13859
|
+
}
|
|
13860
|
+
resolveGitHubGrant(token) {
|
|
13861
|
+
const row = this.db.prepare("SELECT conversation_id, repository FROM aster_github_grants WHERE token = ? AND revoked_at IS NULL").get(token);
|
|
13862
|
+
return row ? { conversationId: row.conversation_id, repository: row.repository } : void 0;
|
|
13863
|
+
}
|
|
13864
|
+
revokeGitHubGrant(conversationId) {
|
|
13865
|
+
this.db.prepare("UPDATE aster_github_grants SET revoked_at = ? WHERE conversation_id = ? AND revoked_at IS NULL").run(Date.now(), conversationId);
|
|
13866
|
+
}
|
|
13867
|
+
/** Switch an idle conversation's model; refused while a turn is active. */
|
|
13868
|
+
setConversationModel(conversationId, model) {
|
|
13869
|
+
const update = this.db.transaction(() => {
|
|
13870
|
+
const now2 = Date.now();
|
|
13871
|
+
const result2 = this.db.prepare("UPDATE aster_conversations SET model = ?, updated_at = ? WHERE id = ? AND active_run_id IS NULL").run(model, now2, conversationId);
|
|
13872
|
+
if (result2.changes !== 1) return false;
|
|
13873
|
+
this.insertEvent(conversationId, null, "model.changed", { model }, now2);
|
|
13874
|
+
return true;
|
|
13875
|
+
});
|
|
13876
|
+
return update.immediate();
|
|
13877
|
+
}
|
|
13821
13878
|
appendEvent(conversationId, runId, type, data) {
|
|
13822
13879
|
const append = this.db.transaction(() => this.insertEvent(conversationId, runId, type, data, Date.now()));
|
|
13823
13880
|
return append.immediate();
|
|
@@ -14720,6 +14777,217 @@ var init_wake_scheduler = __esm({
|
|
|
14720
14777
|
}
|
|
14721
14778
|
});
|
|
14722
14779
|
|
|
14780
|
+
// src/models/openrouter-catalog.ts
|
|
14781
|
+
var openrouter_catalog_exports = {};
|
|
14782
|
+
__export(openrouter_catalog_exports, {
|
|
14783
|
+
DEFAULT_CACHE_TTL_MS: () => DEFAULT_CACHE_TTL_MS,
|
|
14784
|
+
ensureOpenRouterCatalog: () => ensureOpenRouterCatalog,
|
|
14785
|
+
fetchOpenRouterCatalog: () => fetchOpenRouterCatalog,
|
|
14786
|
+
loadOpenRouterCatalog: () => loadOpenRouterCatalog,
|
|
14787
|
+
mapOpenRouterModel: () => mapOpenRouterModel
|
|
14788
|
+
});
|
|
14789
|
+
import { readFile as readFile20, writeFile as writeFile12, mkdir as mkdir13 } from "fs/promises";
|
|
14790
|
+
import { homedir as homedir22 } from "os";
|
|
14791
|
+
import { join as join30, dirname as dirname15 } from "path";
|
|
14792
|
+
function catalogCachePath() {
|
|
14793
|
+
const xdg = process.env.XDG_CONFIG_HOME || join30(homedir22(), ".config");
|
|
14794
|
+
return join30(xdg, "kimiflare", "openrouter-models.json");
|
|
14795
|
+
}
|
|
14796
|
+
function perMtok(perToken) {
|
|
14797
|
+
const n = Number(perToken);
|
|
14798
|
+
return Number.isFinite(n) ? n * 1e6 : 0;
|
|
14799
|
+
}
|
|
14800
|
+
function mapOpenRouterModel(raw) {
|
|
14801
|
+
const supported = new Set(raw.supported_parameters ?? []);
|
|
14802
|
+
const aa = raw.benchmarks?.artificial_analysis;
|
|
14803
|
+
const quality = {
|
|
14804
|
+
...typeof aa?.coding_index === "number" ? { coding: aa.coding_index } : {},
|
|
14805
|
+
...typeof aa?.agentic_index === "number" ? { agentic: aa.agentic_index } : {},
|
|
14806
|
+
...typeof aa?.intelligence_index === "number" ? { intelligence: aa.intelligence_index } : {}
|
|
14807
|
+
};
|
|
14808
|
+
return {
|
|
14809
|
+
id: raw.id,
|
|
14810
|
+
...raw.name ? { name: raw.name } : {},
|
|
14811
|
+
...typeof raw.created === "number" ? { created: raw.created } : {},
|
|
14812
|
+
...Object.keys(quality).length > 0 ? { quality } : {},
|
|
14813
|
+
...raw.supported_parameters ? { parameters: [...raw.supported_parameters] } : {},
|
|
14814
|
+
contextWindow: raw.context_length ?? 128e3,
|
|
14815
|
+
maxOutputTokens: raw.top_provider?.max_completion_tokens ?? 4096,
|
|
14816
|
+
pricing: {
|
|
14817
|
+
inputPerMtok: perMtok(raw.pricing?.prompt),
|
|
14818
|
+
...raw.pricing?.input_cache_read ? { cachedInputPerMtok: perMtok(raw.pricing.input_cache_read) } : {},
|
|
14819
|
+
outputPerMtok: perMtok(raw.pricing?.completion)
|
|
14820
|
+
},
|
|
14821
|
+
supports: {
|
|
14822
|
+
// OpenRouter's own catalog is the authority on whether a model accepts tool
|
|
14823
|
+
// calls; absence of "tools" here means the upstream provider doesn't support
|
|
14824
|
+
// function calling for this model, and we must not claim otherwise.
|
|
14825
|
+
tools: supported.has("tools"),
|
|
14826
|
+
reasoning: supported.has("reasoning") || supported.has("include_reasoning"),
|
|
14827
|
+
streaming: true,
|
|
14828
|
+
vision: (raw.architecture?.input_modalities ?? []).includes("image"),
|
|
14829
|
+
temperature: supported.has("temperature") ? true : void 0
|
|
14830
|
+
}
|
|
14831
|
+
};
|
|
14832
|
+
}
|
|
14833
|
+
async function fetchOpenRouterCatalog(fetchImpl2 = fetch) {
|
|
14834
|
+
const res = await fetchWithNetworkRetry(fetchImpl2, openRouterUrl("models"), {
|
|
14835
|
+
headers: { Accept: "application/json" }
|
|
14836
|
+
});
|
|
14837
|
+
if (!res.ok) {
|
|
14838
|
+
throw new Error(`OpenRouter /models returned HTTP ${res.status}`);
|
|
14839
|
+
}
|
|
14840
|
+
const body = await res.json();
|
|
14841
|
+
return (body.data ?? []).map(mapOpenRouterModel);
|
|
14842
|
+
}
|
|
14843
|
+
async function readCache3() {
|
|
14844
|
+
try {
|
|
14845
|
+
const raw = await readFile20(catalogCachePath(), "utf8");
|
|
14846
|
+
const parsed = JSON.parse(raw);
|
|
14847
|
+
return parsed.version === CACHE_VERSION ? parsed : null;
|
|
14848
|
+
} catch {
|
|
14849
|
+
return null;
|
|
14850
|
+
}
|
|
14851
|
+
}
|
|
14852
|
+
async function writeCache3(models) {
|
|
14853
|
+
const path = catalogCachePath();
|
|
14854
|
+
try {
|
|
14855
|
+
await mkdir13(dirname15(path), { recursive: true });
|
|
14856
|
+
const cache = { version: CACHE_VERSION, fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), models };
|
|
14857
|
+
await writeFile12(path, JSON.stringify(cache, null, 2), "utf8");
|
|
14858
|
+
} catch {
|
|
14859
|
+
}
|
|
14860
|
+
}
|
|
14861
|
+
async function loadOpenRouterCatalog(opts2 = {}) {
|
|
14862
|
+
const ttlMs = opts2.ttlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
14863
|
+
const cached = await readCache3();
|
|
14864
|
+
const isFresh = cached && Date.now() - Date.parse(cached.fetchedAt) < ttlMs;
|
|
14865
|
+
if (isFresh) return cached.models;
|
|
14866
|
+
try {
|
|
14867
|
+
const models = await fetchOpenRouterCatalog(opts2.fetchImpl ?? fetch);
|
|
14868
|
+
await writeCache3(models);
|
|
14869
|
+
return models;
|
|
14870
|
+
} catch {
|
|
14871
|
+
return cached?.models ?? [];
|
|
14872
|
+
}
|
|
14873
|
+
}
|
|
14874
|
+
async function ensureOpenRouterCatalog(opts2 = {}) {
|
|
14875
|
+
try {
|
|
14876
|
+
const models = await loadOpenRouterCatalog(opts2);
|
|
14877
|
+
if (models.length > 0) registerOpenRouterModels(models);
|
|
14878
|
+
return models.length;
|
|
14879
|
+
} catch {
|
|
14880
|
+
return 0;
|
|
14881
|
+
}
|
|
14882
|
+
}
|
|
14883
|
+
var CACHE_VERSION, DEFAULT_CACHE_TTL_MS;
|
|
14884
|
+
var init_openrouter_catalog = __esm({
|
|
14885
|
+
"src/models/openrouter-catalog.ts"() {
|
|
14886
|
+
"use strict";
|
|
14887
|
+
init_registry();
|
|
14888
|
+
init_openrouter();
|
|
14889
|
+
CACHE_VERSION = 4;
|
|
14890
|
+
DEFAULT_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
14891
|
+
}
|
|
14892
|
+
});
|
|
14893
|
+
|
|
14894
|
+
// src/server/aster-github.ts
|
|
14895
|
+
function mapRepository(raw) {
|
|
14896
|
+
return {
|
|
14897
|
+
fullName: raw.full_name,
|
|
14898
|
+
name: raw.name,
|
|
14899
|
+
owner: raw.owner?.login ?? raw.full_name.split("/")[0] ?? "",
|
|
14900
|
+
private: raw.private,
|
|
14901
|
+
description: raw.description,
|
|
14902
|
+
defaultBranch: raw.default_branch ?? "main",
|
|
14903
|
+
updatedAt: raw.pushed_at ?? raw.updated_at ?? null
|
|
14904
|
+
};
|
|
14905
|
+
}
|
|
14906
|
+
var AsterGitHubError, REPOSITORY_FULL_NAME_RE, REPOSITORY_NAME_RE, API, AsterGitHub;
|
|
14907
|
+
var init_aster_github = __esm({
|
|
14908
|
+
"src/server/aster-github.ts"() {
|
|
14909
|
+
"use strict";
|
|
14910
|
+
AsterGitHubError = class extends Error {
|
|
14911
|
+
constructor(message2, status, code) {
|
|
14912
|
+
super(message2);
|
|
14913
|
+
this.status = status;
|
|
14914
|
+
this.code = code;
|
|
14915
|
+
this.name = "AsterGitHubError";
|
|
14916
|
+
}
|
|
14917
|
+
status;
|
|
14918
|
+
code;
|
|
14919
|
+
};
|
|
14920
|
+
REPOSITORY_FULL_NAME_RE = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/[A-Za-z0-9._-]{1,100}$/;
|
|
14921
|
+
REPOSITORY_NAME_RE = /^[A-Za-z0-9._-]{1,100}$/;
|
|
14922
|
+
API = "https://api.github.com";
|
|
14923
|
+
AsterGitHub = class {
|
|
14924
|
+
constructor(token = process.env.GITHUB_TOKEN, fetchImpl2 = fetch) {
|
|
14925
|
+
this.token = token;
|
|
14926
|
+
this.fetchImpl = fetchImpl2;
|
|
14927
|
+
}
|
|
14928
|
+
token;
|
|
14929
|
+
fetchImpl;
|
|
14930
|
+
get configured() {
|
|
14931
|
+
return Boolean(this.token?.trim());
|
|
14932
|
+
}
|
|
14933
|
+
/** Repositories the token can push to, most recently pushed first. */
|
|
14934
|
+
async listRepositories(query = "") {
|
|
14935
|
+
const raw = await this.request(
|
|
14936
|
+
"GET",
|
|
14937
|
+
"/user/repos?per_page=100&sort=pushed&affiliation=owner,collaborator,organization_member"
|
|
14938
|
+
);
|
|
14939
|
+
const needle = query.trim().toLowerCase();
|
|
14940
|
+
return raw.map(mapRepository).filter((repo) => !needle || repo.fullName.toLowerCase().includes(needle) || (repo.description ?? "").toLowerCase().includes(needle));
|
|
14941
|
+
}
|
|
14942
|
+
async getRepository(fullName) {
|
|
14943
|
+
if (!REPOSITORY_FULL_NAME_RE.test(fullName)) throw new AsterGitHubError("Invalid repository name", 400, "invalid_repository");
|
|
14944
|
+
return mapRepository(await this.request("GET", `/repos/${fullName}`));
|
|
14945
|
+
}
|
|
14946
|
+
/** Creates a repository under the token's user, initialized so it has a default branch to clone. */
|
|
14947
|
+
async createRepository(input) {
|
|
14948
|
+
if (!REPOSITORY_NAME_RE.test(input.name) || input.name === "." || input.name === "..") {
|
|
14949
|
+
throw new AsterGitHubError("Repository names may use letters, digits, '.', '_' and '-'", 400, "invalid_repository_name");
|
|
14950
|
+
}
|
|
14951
|
+
const description = input.description?.trim().slice(0, 350);
|
|
14952
|
+
return mapRepository(await this.request("POST", "/user/repos", {
|
|
14953
|
+
name: input.name,
|
|
14954
|
+
private: input.private,
|
|
14955
|
+
auto_init: true,
|
|
14956
|
+
...description ? { description } : {}
|
|
14957
|
+
}));
|
|
14958
|
+
}
|
|
14959
|
+
async request(method, path, body) {
|
|
14960
|
+
if (!this.configured) throw new AsterGitHubError("GitHub is not connected on the server", 503, "github_not_configured");
|
|
14961
|
+
let response;
|
|
14962
|
+
try {
|
|
14963
|
+
response = await this.fetchImpl(API + path, {
|
|
14964
|
+
method,
|
|
14965
|
+
headers: {
|
|
14966
|
+
Accept: "application/vnd.github+json",
|
|
14967
|
+
Authorization: `Bearer ${this.token}`,
|
|
14968
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
14969
|
+
"User-Agent": "autopilot-aster",
|
|
14970
|
+
...body === void 0 ? {} : { "Content-Type": "application/json" }
|
|
14971
|
+
},
|
|
14972
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
14973
|
+
signal: AbortSignal.timeout(2e4)
|
|
14974
|
+
});
|
|
14975
|
+
} catch {
|
|
14976
|
+
throw new AsterGitHubError("GitHub could not be reached", 502, "github_unreachable");
|
|
14977
|
+
}
|
|
14978
|
+
if (response.ok) return await response.json();
|
|
14979
|
+
const detail = await response.json().catch(() => ({}));
|
|
14980
|
+
const message2 = detail.errors?.[0]?.message ?? detail.message ?? `GitHub returned HTTP ${response.status}`;
|
|
14981
|
+
if (response.status === 404) throw new AsterGitHubError("Repository not found or not accessible with the server's GitHub token", 404, "repository_not_found");
|
|
14982
|
+
if (response.status === 401) throw new AsterGitHubError("The server's GitHub token was rejected", 502, "github_token_rejected");
|
|
14983
|
+
if (response.status === 403) throw new AsterGitHubError(`GitHub refused the request: ${message2}`, 403, "github_forbidden");
|
|
14984
|
+
if (response.status === 422) throw new AsterGitHubError(message2, 422, "github_validation_failed");
|
|
14985
|
+
throw new AsterGitHubError(message2, 502, "github_error");
|
|
14986
|
+
}
|
|
14987
|
+
};
|
|
14988
|
+
}
|
|
14989
|
+
});
|
|
14990
|
+
|
|
14723
14991
|
// src/server/aster-tools.ts
|
|
14724
14992
|
import { realpath as realpath5 } from "fs/promises";
|
|
14725
14993
|
import { isAbsolute as isAbsolute5, relative as relative7, sep as sep5 } from "path";
|
|
@@ -14785,11 +15053,18 @@ function posixNormalize(value) {
|
|
|
14785
15053
|
}
|
|
14786
15054
|
return "/" + out.join("/");
|
|
14787
15055
|
}
|
|
15056
|
+
function isLikelySkKey(candidate) {
|
|
15057
|
+
if (/^sk-or-v1-[A-Za-z0-9]{16,}/.test(candidate)) return true;
|
|
15058
|
+
return candidate.slice(3).split(/[-_]/).some((chunk) => chunk.length >= 20 && /\d/.test(chunk));
|
|
15059
|
+
}
|
|
14788
15060
|
function containsLikelyProviderSecret(value) {
|
|
14789
|
-
|
|
15061
|
+
for (const match of value.matchAll(SK_CANDIDATE_RE)) {
|
|
15062
|
+
if (isLikelySkKey(match[0])) return true;
|
|
15063
|
+
}
|
|
15064
|
+
return /(?:\bgh[pousr]_[A-Za-z0-9]{20,}|\bgithub_pat_[A-Za-z0-9_]{20,}|(?:CLOUDFLARE|OPENROUTER|REQUESTY)_API_(?:KEY|TOKEN)\s*=)/.test(value);
|
|
14790
15065
|
}
|
|
14791
15066
|
function redactLikelySecrets(value) {
|
|
14792
|
-
return value.replace(
|
|
15067
|
+
return value.replace(SK_CANDIDATE_RE, (match) => isLikelySkKey(match) ? "[REDACTED_PROVIDER_SECRET]" : match).replace(/\bgh[pousr]_[A-Za-z0-9]{20,}/g, "[REDACTED_PROVIDER_SECRET]").replace(/\bgithub_pat_[A-Za-z0-9_]{20,}/g, "[REDACTED_PROVIDER_SECRET]");
|
|
14793
15068
|
}
|
|
14794
15069
|
function assertInside(root, candidate) {
|
|
14795
15070
|
const rel = relative7(root, candidate);
|
|
@@ -14801,7 +15076,7 @@ function assertNotSensitive(root, candidate) {
|
|
|
14801
15076
|
throw new Error("workspace_sensitive_path_forbidden");
|
|
14802
15077
|
}
|
|
14803
15078
|
}
|
|
14804
|
-
var ASTR_TOOLS, SENSITIVE_NAMES;
|
|
15079
|
+
var ASTR_TOOLS, SENSITIVE_NAMES, SK_CANDIDATE_RE;
|
|
14805
15080
|
var init_aster_tools = __esm({
|
|
14806
15081
|
"src/server/aster-tools.ts"() {
|
|
14807
15082
|
"use strict";
|
|
@@ -14824,11 +15099,519 @@ var init_aster_tools = __esm({
|
|
|
14824
15099
|
"credentials",
|
|
14825
15100
|
"secrets"
|
|
14826
15101
|
]);
|
|
15102
|
+
SK_CANDIDATE_RE = /(?<![A-Za-z0-9])sk-[A-Za-z0-9_-]{16,}/g;
|
|
14827
15103
|
}
|
|
14828
15104
|
});
|
|
14829
15105
|
|
|
14830
|
-
// src/server/aster-
|
|
15106
|
+
// src/server/aster-memory.ts
|
|
15107
|
+
import Database4 from "better-sqlite3";
|
|
14831
15108
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
15109
|
+
import { chmodSync as chmodSync3, mkdirSync as mkdirSync7 } from "fs";
|
|
15110
|
+
import { dirname as dirname16 } from "path";
|
|
15111
|
+
function rowToItem(row) {
|
|
15112
|
+
return {
|
|
15113
|
+
id: row.id,
|
|
15114
|
+
kind: row.kind,
|
|
15115
|
+
key: row.key,
|
|
15116
|
+
title: row.title,
|
|
15117
|
+
content: row.content,
|
|
15118
|
+
pinned: row.pinned === 1,
|
|
15119
|
+
sourceConversationId: row.source_conversation_id,
|
|
15120
|
+
createdAt: row.created_at,
|
|
15121
|
+
updatedAt: row.updated_at
|
|
15122
|
+
};
|
|
15123
|
+
}
|
|
15124
|
+
function normalizeKey(kind, value) {
|
|
15125
|
+
const raw = value.toLowerCase().startsWith(`${kind}:`) ? value.slice(kind.length + 1) : value;
|
|
15126
|
+
const slug = raw.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
15127
|
+
const key = `${kind}:${slug}`;
|
|
15128
|
+
return KEY_RE.test(key) ? key : null;
|
|
15129
|
+
}
|
|
15130
|
+
function formatCard(card) {
|
|
15131
|
+
return `- ${card.title} (${card.kind}): ${card.content}`;
|
|
15132
|
+
}
|
|
15133
|
+
function mentions(text, card) {
|
|
15134
|
+
const haystack = ` ${text.toLowerCase()} `;
|
|
15135
|
+
const words = /* @__PURE__ */ new Set();
|
|
15136
|
+
for (const part of [card.title, card.key?.split(":")[1]?.replace(/-/g, " ") ?? ""]) {
|
|
15137
|
+
for (const word of part.toLowerCase().split(/[^\p{L}\p{N}]+/u)) {
|
|
15138
|
+
if (word.length >= 3 && !STOP_WORDS.has(word)) words.add(word);
|
|
15139
|
+
}
|
|
15140
|
+
}
|
|
15141
|
+
return [...words].some((word) => new RegExp(`[^\\p{L}\\p{N}]${escapeRegExp(word)}[^\\p{L}\\p{N}]`, "u").test(haystack));
|
|
15142
|
+
}
|
|
15143
|
+
function escapeRegExp(value) {
|
|
15144
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
15145
|
+
}
|
|
15146
|
+
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;
|
|
15147
|
+
var init_aster_memory = __esm({
|
|
15148
|
+
"src/server/aster-memory.ts"() {
|
|
15149
|
+
"use strict";
|
|
15150
|
+
init_openrouter();
|
|
15151
|
+
init_aster_store();
|
|
15152
|
+
init_aster_tools();
|
|
15153
|
+
MAX_CARD_CHARS = 900;
|
|
15154
|
+
MAX_FACT_CHARS = 300;
|
|
15155
|
+
MAX_MEMORY_PROMPT_ITEMS = 150;
|
|
15156
|
+
BRIEF_FACTS = 15;
|
|
15157
|
+
BRIEF_RECENT_SESSIONS = 6;
|
|
15158
|
+
BRIEF_RECENT_CARDS = 3;
|
|
15159
|
+
KEY_RE = /^(person|project):[a-z0-9][a-z0-9-]{0,80}$/;
|
|
15160
|
+
AsterMemory = class {
|
|
15161
|
+
constructor(dbPath = defaultAsterDbPath(), options = {}) {
|
|
15162
|
+
this.options = options;
|
|
15163
|
+
mkdirSync7(dirname16(dbPath), { recursive: true, mode: 448 });
|
|
15164
|
+
this.db = new Database4(dbPath);
|
|
15165
|
+
this.db.pragma("journal_mode = WAL");
|
|
15166
|
+
this.db.pragma("busy_timeout = 5000");
|
|
15167
|
+
this.db.exec(`
|
|
15168
|
+
CREATE TABLE IF NOT EXISTS aster_memories (
|
|
15169
|
+
id TEXT PRIMARY KEY,
|
|
15170
|
+
kind TEXT NOT NULL,
|
|
15171
|
+
key TEXT,
|
|
15172
|
+
title TEXT NOT NULL,
|
|
15173
|
+
content TEXT NOT NULL,
|
|
15174
|
+
pinned INTEGER NOT NULL DEFAULT 0,
|
|
15175
|
+
source_conversation_id TEXT,
|
|
15176
|
+
created_at INTEGER NOT NULL,
|
|
15177
|
+
updated_at INTEGER NOT NULL,
|
|
15178
|
+
deleted INTEGER NOT NULL DEFAULT 0
|
|
15179
|
+
);
|
|
15180
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_aster_memories_key ON aster_memories(key) WHERE key IS NOT NULL AND deleted = 0;
|
|
15181
|
+
CREATE INDEX IF NOT EXISTS idx_aster_memories_kind ON aster_memories(kind, deleted, updated_at);
|
|
15182
|
+
`);
|
|
15183
|
+
try {
|
|
15184
|
+
chmodSync3(dbPath, 384);
|
|
15185
|
+
} catch {
|
|
15186
|
+
}
|
|
15187
|
+
}
|
|
15188
|
+
options;
|
|
15189
|
+
db;
|
|
15190
|
+
/** Card keys already shown to the agent per conversation, so later turns only add new ones. */
|
|
15191
|
+
shown = /* @__PURE__ */ new Map();
|
|
15192
|
+
get enabled() {
|
|
15193
|
+
return this.options.enabled !== false && Boolean(this.apiKey);
|
|
15194
|
+
}
|
|
15195
|
+
get apiKey() {
|
|
15196
|
+
return this.options.apiKey ?? process.env.OPENROUTER_API_KEY;
|
|
15197
|
+
}
|
|
15198
|
+
get model() {
|
|
15199
|
+
return this.options.model ?? process.env.AUTOPILOT_ASTER_MEMORY_MODEL ?? "google/gemini-3.8-flash";
|
|
15200
|
+
}
|
|
15201
|
+
close() {
|
|
15202
|
+
this.db.close();
|
|
15203
|
+
}
|
|
15204
|
+
list(kind) {
|
|
15205
|
+
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();
|
|
15206
|
+
return rows.map(rowToItem);
|
|
15207
|
+
}
|
|
15208
|
+
get(id) {
|
|
15209
|
+
const row = this.db.prepare("SELECT * FROM aster_memories WHERE id = ? AND deleted = 0").get(id);
|
|
15210
|
+
return row ? rowToItem(row) : void 0;
|
|
15211
|
+
}
|
|
15212
|
+
/** Soft delete; the user removed it, so extraction must not silently recreate it from this item. */
|
|
15213
|
+
delete(id) {
|
|
15214
|
+
return this.db.prepare("UPDATE aster_memories SET deleted = 1, updated_at = ? WHERE id = ? AND deleted = 0").run(Date.now(), id).changes === 1;
|
|
15215
|
+
}
|
|
15216
|
+
/** Write or merge one item. person/project/session are keyed; facts are by id. */
|
|
15217
|
+
upsert(input) {
|
|
15218
|
+
const now2 = Date.now();
|
|
15219
|
+
const limit = input.kind === "fact" ? MAX_FACT_CHARS : MAX_CARD_CHARS;
|
|
15220
|
+
const title = input.title.trim().slice(0, 120);
|
|
15221
|
+
const content = input.content.trim().slice(0, limit);
|
|
15222
|
+
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;
|
|
15223
|
+
if (existing) {
|
|
15224
|
+
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);
|
|
15225
|
+
return { item: this.get(existing.id), change: "updated" };
|
|
15226
|
+
}
|
|
15227
|
+
const id = randomUUID6();
|
|
15228
|
+
this.db.prepare(`INSERT INTO aster_memories (id, kind, key, title, content, pinned, source_conversation_id, created_at, updated_at)
|
|
15229
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, input.kind, input.key ?? null, title, content, input.pinned ? 1 : 0, input.sourceConversationId ?? null, now2, now2);
|
|
15230
|
+
return { item: this.get(id), change: "added" };
|
|
15231
|
+
}
|
|
15232
|
+
/**
|
|
15233
|
+
* The memory note for a turn: a full brief on a conversation's first turn, afterwards only
|
|
15234
|
+
* person/project cards the user's message newly brings up. Empty when there is nothing new.
|
|
15235
|
+
*/
|
|
15236
|
+
brief(conversationId, userText) {
|
|
15237
|
+
const items = this.list();
|
|
15238
|
+
if (items.length === 0) return "";
|
|
15239
|
+
const shown = this.shown.get(conversationId);
|
|
15240
|
+
const cards = items.filter((item) => item.kind === "person" || item.kind === "project");
|
|
15241
|
+
const mentioned = cards.filter((card) => mentions(userText, card));
|
|
15242
|
+
if (shown) {
|
|
15243
|
+
const fresh = mentioned.filter((card) => !shown.has(card.id));
|
|
15244
|
+
if (fresh.length === 0) return "";
|
|
15245
|
+
fresh.forEach((card) => shown.add(card.id));
|
|
15246
|
+
return `[Aster memory \u2014 relevant to this message]
|
|
15247
|
+
${fresh.map(formatCard).join("\n")}`;
|
|
15248
|
+
}
|
|
15249
|
+
const shownNow = /* @__PURE__ */ new Set();
|
|
15250
|
+
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);
|
|
15251
|
+
const featured = [...mentioned, ...cards.filter((card) => !mentioned.includes(card)).slice(0, BRIEF_RECENT_CARDS)];
|
|
15252
|
+
featured.forEach((card) => shownNow.add(card.id));
|
|
15253
|
+
const others = cards.filter((card) => !shownNow.has(card.id));
|
|
15254
|
+
const sessions = items.filter((item) => item.kind === "session" && item.key !== conversationId).slice(0, BRIEF_RECENT_SESSIONS);
|
|
15255
|
+
this.shown.set(conversationId, shownNow);
|
|
15256
|
+
const sections = [
|
|
15257
|
+
"[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.]"
|
|
15258
|
+
];
|
|
15259
|
+
if (facts.length) sections.push("About the user:\n" + facts.map((fact) => `- ${fact.content}`).join("\n"));
|
|
15260
|
+
if (featured.length) sections.push("People and projects:\n" + featured.map(formatCard).join("\n"));
|
|
15261
|
+
if (others.length) sections.push("Also known (details in memory.md): " + others.map((card) => card.title).join(", "));
|
|
15262
|
+
if (sessions.length) sections.push("Recent chats:\n" + sessions.map((session) => `- ${session.title}: ${session.content}`).join("\n"));
|
|
15263
|
+
return sections.join("\n\n");
|
|
15264
|
+
}
|
|
15265
|
+
/** All memory as Markdown, written into the cell so the agent can search it on demand. */
|
|
15266
|
+
snapshot() {
|
|
15267
|
+
const items = this.list();
|
|
15268
|
+
const group = (kind, heading, render2) => {
|
|
15269
|
+
const entries = items.filter((item) => item.kind === kind);
|
|
15270
|
+
return entries.length ? `## ${heading}
|
|
15271
|
+
|
|
15272
|
+
${entries.map(render2).join("\n")}
|
|
15273
|
+
` : "";
|
|
15274
|
+
};
|
|
15275
|
+
return [
|
|
15276
|
+
"# Aster memory\n\nWhat the user's assistant remembers across chats. Read-only here; it is updated automatically after each turn.\n",
|
|
15277
|
+
group("fact", "About the user", (item) => `- ${item.content}`),
|
|
15278
|
+
group("person", "People", (item) => `### ${item.title}
|
|
15279
|
+
${item.content}
|
|
15280
|
+
`),
|
|
15281
|
+
group("project", "Projects", (item) => `### ${item.title}
|
|
15282
|
+
${item.content}
|
|
15283
|
+
`),
|
|
15284
|
+
group("session", "Chats", (item) => `- **${item.title}** (${new Date(item.updatedAt).toISOString().slice(0, 10)}): ${item.content}`)
|
|
15285
|
+
].filter(Boolean).join("\n");
|
|
15286
|
+
}
|
|
15287
|
+
/** Read one exchange and update memory. Returns what changed (for the app's "memory updated" chip). */
|
|
15288
|
+
async remember(exchange) {
|
|
15289
|
+
if (!this.enabled) return [];
|
|
15290
|
+
const result2 = await this.extract(exchange);
|
|
15291
|
+
if (!result2) return [];
|
|
15292
|
+
const changes = [];
|
|
15293
|
+
const known = new Map(this.list().filter((item) => item.kind !== "session").map((item) => [item.id, item]));
|
|
15294
|
+
for (const id of result2.deletes ?? []) {
|
|
15295
|
+
const item = known.get(id);
|
|
15296
|
+
if (item && !item.pinned && this.delete(id)) changes.push({ id, kind: item.kind, title: item.title, change: "removed" });
|
|
15297
|
+
}
|
|
15298
|
+
for (const upsert of result2.upserts ?? []) {
|
|
15299
|
+
const kind = upsert.kind === "person" || upsert.kind === "project" || upsert.kind === "fact" ? upsert.kind : void 0;
|
|
15300
|
+
const title = upsert.title?.trim();
|
|
15301
|
+
const content = upsert.content?.trim();
|
|
15302
|
+
if (!kind || !title || !content || containsLikelyProviderSecret(content)) continue;
|
|
15303
|
+
const key = kind === "fact" ? null : normalizeKey(kind, upsert.key ?? title);
|
|
15304
|
+
if (kind !== "fact" && !key) continue;
|
|
15305
|
+
const id = kind === "fact" && upsert.id && known.has(upsert.id) ? upsert.id : null;
|
|
15306
|
+
const { item, change } = this.upsert({ kind, key, id, title, content, pinned: upsert.pinned === true, sourceConversationId: exchange.conversationId });
|
|
15307
|
+
changes.push({ id: item.id, kind, title: item.title, change });
|
|
15308
|
+
}
|
|
15309
|
+
const session = result2.session;
|
|
15310
|
+
if (session?.title?.trim() && session.summary?.trim()) {
|
|
15311
|
+
const repo = exchange.project?.kind === "code" && exchange.project.repository ? ` [${exchange.project.repository}]` : "";
|
|
15312
|
+
this.upsert({
|
|
15313
|
+
kind: "session",
|
|
15314
|
+
key: exchange.conversationId,
|
|
15315
|
+
title: session.title.trim().slice(0, 80) + repo,
|
|
15316
|
+
content: session.summary.trim(),
|
|
15317
|
+
sourceConversationId: exchange.conversationId
|
|
15318
|
+
});
|
|
15319
|
+
}
|
|
15320
|
+
return changes;
|
|
15321
|
+
}
|
|
15322
|
+
async extract(exchange) {
|
|
15323
|
+
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);
|
|
15324
|
+
const currentSession = this.list("session").find((item) => item.key === exchange.conversationId);
|
|
15325
|
+
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)";
|
|
15326
|
+
const context = exchange.project?.kind === "code" && exchange.project.repository ? `Code chat in GitHub repository ${exchange.project.repository}.` : "General chat.";
|
|
15327
|
+
const user = [
|
|
15328
|
+
`CURRENT MEMORY (one JSON object per line):
|
|
15329
|
+
${memory}`,
|
|
15330
|
+
`THIS CHAT: ${context}${currentSession ? ` Summary so far: ${currentSession.content}` : ""}`,
|
|
15331
|
+
`LATEST USER MESSAGE:
|
|
15332
|
+
${exchange.userText.slice(0, 6e3)}`,
|
|
15333
|
+
`ASSISTANT REPLY:
|
|
15334
|
+
${exchange.assistantText.slice(0, 4e3)}`
|
|
15335
|
+
].join("\n\n");
|
|
15336
|
+
let response;
|
|
15337
|
+
try {
|
|
15338
|
+
response = await (this.options.fetchImpl ?? fetch)(openRouterUrl("chat/completions"), {
|
|
15339
|
+
method: "POST",
|
|
15340
|
+
headers: { ...openRouterHeaders(this.apiKey), "Content-Type": "application/json" },
|
|
15341
|
+
body: JSON.stringify({
|
|
15342
|
+
model: this.model,
|
|
15343
|
+
temperature: 0,
|
|
15344
|
+
response_format: { type: "json_object" },
|
|
15345
|
+
messages: [{ role: "system", content: EXTRACTION_PROMPT }, { role: "user", content: user }]
|
|
15346
|
+
}),
|
|
15347
|
+
signal: AbortSignal.timeout(3e4)
|
|
15348
|
+
});
|
|
15349
|
+
} catch {
|
|
15350
|
+
return void 0;
|
|
15351
|
+
}
|
|
15352
|
+
if (!response.ok) return void 0;
|
|
15353
|
+
const body = await response.json().catch(() => void 0);
|
|
15354
|
+
const text = body?.choices?.[0]?.message?.content;
|
|
15355
|
+
if (!text) return void 0;
|
|
15356
|
+
try {
|
|
15357
|
+
return JSON.parse(text.replace(/^```(?:json)?\s*|\s*```$/g, ""));
|
|
15358
|
+
} catch {
|
|
15359
|
+
return void 0;
|
|
15360
|
+
}
|
|
15361
|
+
}
|
|
15362
|
+
};
|
|
15363
|
+
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.
|
|
15364
|
+
|
|
15365
|
+
Remember what will matter in future, unrelated chats:
|
|
15366
|
+
- 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.
|
|
15367
|
+
- PROJECTS: name, what it is, client or owner, goals, scope, stack, repository, status, key decisions, and ideas the user brainstormed or chose.
|
|
15368
|
+
- FACTS about the user: durable preferences, circumstances (job, freelance work, location if stated), ways of working, tools they use.
|
|
15369
|
+
|
|
15370
|
+
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.
|
|
15371
|
+
|
|
15372
|
+
Rules:
|
|
15373
|
+
- If the user explicitly asks to remember something, you MUST store it, with "pinned": true.
|
|
15374
|
+
- 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).
|
|
15375
|
+
- Keys: "person:<first-last>" or "project:<short-name>", lowercase with hyphens.
|
|
15376
|
+
- 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".
|
|
15377
|
+
- Most exchanges contain nothing worth remembering: return empty "upserts" then. Never invent details.
|
|
15378
|
+
- 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.
|
|
15379
|
+
|
|
15380
|
+
Return only JSON:
|
|
15381
|
+
{"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":"..."}}`;
|
|
15382
|
+
STOP_WORDS = /* @__PURE__ */ new Set(["the", "and", "for", "app", "project", "new", "with", "website", "site"]);
|
|
15383
|
+
}
|
|
15384
|
+
});
|
|
15385
|
+
|
|
15386
|
+
// src/server/aster-github-proxy.ts
|
|
15387
|
+
import { createServer } from "http";
|
|
15388
|
+
import { request as httpsRequest } from "https";
|
|
15389
|
+
import { request as httpRequest } from "http";
|
|
15390
|
+
function extractToken(req) {
|
|
15391
|
+
const header = req.headers.authorization;
|
|
15392
|
+
if (!header) return void 0;
|
|
15393
|
+
const [scheme, value = ""] = header.split(" ", 2);
|
|
15394
|
+
if (/^(bearer|token)$/i.test(scheme ?? "")) return value.trim() || void 0;
|
|
15395
|
+
if (/^basic$/i.test(scheme ?? "")) {
|
|
15396
|
+
const decoded = Buffer.from(value, "base64").toString("utf8");
|
|
15397
|
+
const password = decoded.slice(decoded.indexOf(":") + 1);
|
|
15398
|
+
return password || void 0;
|
|
15399
|
+
}
|
|
15400
|
+
return void 0;
|
|
15401
|
+
}
|
|
15402
|
+
function deny(res, status, message2) {
|
|
15403
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
15404
|
+
res.end(JSON.stringify({ message: message2 }));
|
|
15405
|
+
}
|
|
15406
|
+
var GIT_PATH_RE, API_PATH_RE, FORWARDED_REQUEST_HEADERS, HOP_BY_HOP, AsterGitHubProxy;
|
|
15407
|
+
var init_aster_github_proxy = __esm({
|
|
15408
|
+
"src/server/aster-github-proxy.ts"() {
|
|
15409
|
+
"use strict";
|
|
15410
|
+
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)$/;
|
|
15411
|
+
API_PATH_RE = /^\/api\/repos\/([A-Za-z0-9-]{1,39})\/([A-Za-z0-9._-]{1,100})(\/.*)?$/;
|
|
15412
|
+
FORWARDED_REQUEST_HEADERS = ["accept", "content-type", "content-encoding", "content-length", "git-protocol", "user-agent", "x-github-api-version"];
|
|
15413
|
+
HOP_BY_HOP = /* @__PURE__ */ new Set(["connection", "keep-alive", "transfer-encoding", "upgrade", "proxy-authenticate", "proxy-authorization", "te", "trailer"]);
|
|
15414
|
+
AsterGitHubProxy = class {
|
|
15415
|
+
constructor(options) {
|
|
15416
|
+
this.options = options;
|
|
15417
|
+
this.gitUpstream = new URL(options.gitUpstream ?? "https://github.com");
|
|
15418
|
+
this.apiUpstream = new URL(options.apiUpstream ?? "https://api.github.com");
|
|
15419
|
+
}
|
|
15420
|
+
options;
|
|
15421
|
+
server;
|
|
15422
|
+
gitUpstream;
|
|
15423
|
+
apiUpstream;
|
|
15424
|
+
async listen(port, host) {
|
|
15425
|
+
this.server = createServer((req, res) => this.handle(req, res));
|
|
15426
|
+
await new Promise((resolve15, reject) => {
|
|
15427
|
+
this.server.once("error", reject);
|
|
15428
|
+
this.server.listen(port, host, () => resolve15());
|
|
15429
|
+
});
|
|
15430
|
+
const address = this.server.address();
|
|
15431
|
+
return typeof address === "object" && address ? address.port : port;
|
|
15432
|
+
}
|
|
15433
|
+
close() {
|
|
15434
|
+
this.server?.close();
|
|
15435
|
+
this.server = void 0;
|
|
15436
|
+
}
|
|
15437
|
+
handle(req, res) {
|
|
15438
|
+
const url = new URL(req.url ?? "/", "http://proxy.local");
|
|
15439
|
+
const git2 = url.pathname.match(GIT_PATH_RE);
|
|
15440
|
+
const api = git2 ? null : url.pathname.match(API_PATH_RE);
|
|
15441
|
+
if (!git2 && !api) return deny(res, 404, "Only this conversation's repository is reachable");
|
|
15442
|
+
const grantToken = extractToken(req);
|
|
15443
|
+
if (!grantToken) {
|
|
15444
|
+
res.writeHead(401, { "www-authenticate": 'Basic realm="aster-github"', "content-type": "text/plain" });
|
|
15445
|
+
res.end("credentials required");
|
|
15446
|
+
return;
|
|
15447
|
+
}
|
|
15448
|
+
const grant = this.options.resolveGrant(grantToken);
|
|
15449
|
+
if (!grant) return deny(res, 403, "This repository grant is invalid or was revoked");
|
|
15450
|
+
const [, owner, name] = git2 ?? api;
|
|
15451
|
+
if (`${owner}/${name}`.toLowerCase() !== grant.repository.toLowerCase()) {
|
|
15452
|
+
return deny(res, 403, `This conversation can only access ${grant.repository}`);
|
|
15453
|
+
}
|
|
15454
|
+
const upstream = git2 ? new URL(`/${owner}/${name}.git/${git2[3]}${url.search}`, this.gitUpstream) : new URL(`/repos/${owner}/${name}${api[3] ?? ""}${url.search}`, this.apiUpstream);
|
|
15455
|
+
const headers = {};
|
|
15456
|
+
for (const name2 of FORWARDED_REQUEST_HEADERS) {
|
|
15457
|
+
const value = req.headers[name2];
|
|
15458
|
+
if (typeof value === "string") headers[name2] = value;
|
|
15459
|
+
}
|
|
15460
|
+
headers["user-agent"] ??= "autopilot-aster";
|
|
15461
|
+
headers.authorization = git2 ? "Basic " + Buffer.from(`x-access-token:${this.options.token}`).toString("base64") : `Bearer ${this.options.token}`;
|
|
15462
|
+
const send = upstream.protocol === "https:" ? httpsRequest : httpRequest;
|
|
15463
|
+
const forward = send(upstream, { method: req.method, headers }, (upstreamRes) => {
|
|
15464
|
+
const responseHeaders = {};
|
|
15465
|
+
for (const [key, value] of Object.entries(upstreamRes.headers)) {
|
|
15466
|
+
if (value !== void 0 && !HOP_BY_HOP.has(key)) responseHeaders[key] = value;
|
|
15467
|
+
}
|
|
15468
|
+
res.writeHead(upstreamRes.statusCode ?? 502, responseHeaders);
|
|
15469
|
+
upstreamRes.pipe(res);
|
|
15470
|
+
});
|
|
15471
|
+
forward.on("error", () => {
|
|
15472
|
+
if (!res.headersSent) deny(res, 502, "GitHub could not be reached");
|
|
15473
|
+
else res.destroy();
|
|
15474
|
+
});
|
|
15475
|
+
req.pipe(forward);
|
|
15476
|
+
}
|
|
15477
|
+
};
|
|
15478
|
+
}
|
|
15479
|
+
});
|
|
15480
|
+
|
|
15481
|
+
// src/server/aster-push.ts
|
|
15482
|
+
import Database5 from "better-sqlite3";
|
|
15483
|
+
import { createSign } from "crypto";
|
|
15484
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
15485
|
+
import { connect } from "http2";
|
|
15486
|
+
function truncate2(value, limit) {
|
|
15487
|
+
const clean = value.replace(/\s+/g, " ").trim();
|
|
15488
|
+
return clean.length > limit ? clean.slice(0, limit - 1) + "\u2026" : clean;
|
|
15489
|
+
}
|
|
15490
|
+
var DEVICE_TOKEN_RE, AsterPush;
|
|
15491
|
+
var init_aster_push = __esm({
|
|
15492
|
+
"src/server/aster-push.ts"() {
|
|
15493
|
+
"use strict";
|
|
15494
|
+
init_aster_store();
|
|
15495
|
+
DEVICE_TOKEN_RE = /^[0-9a-f]{64,200}$/i;
|
|
15496
|
+
AsterPush = class {
|
|
15497
|
+
db;
|
|
15498
|
+
options;
|
|
15499
|
+
privateKey;
|
|
15500
|
+
jwt;
|
|
15501
|
+
sessions = /* @__PURE__ */ new Map();
|
|
15502
|
+
constructor(dbPath = defaultAsterDbPath(), options = {}, env2 = process.env) {
|
|
15503
|
+
this.options = {
|
|
15504
|
+
...options,
|
|
15505
|
+
keyPath: options.keyPath ?? env2.AUTOPILOT_ASTER_APNS_KEY_PATH,
|
|
15506
|
+
keyId: options.keyId ?? env2.AUTOPILOT_ASTER_APNS_KEY_ID ?? "",
|
|
15507
|
+
teamId: options.teamId ?? env2.AUTOPILOT_ASTER_APNS_TEAM_ID ?? "",
|
|
15508
|
+
topic: options.topic ?? env2.AUTOPILOT_ASTER_APNS_TOPIC ?? "com.aster.ai"
|
|
15509
|
+
};
|
|
15510
|
+
this.db = new Database5(dbPath);
|
|
15511
|
+
this.db.pragma("journal_mode = WAL");
|
|
15512
|
+
this.db.pragma("busy_timeout = 5000");
|
|
15513
|
+
this.db.exec(`
|
|
15514
|
+
CREATE TABLE IF NOT EXISTS aster_devices (
|
|
15515
|
+
token TEXT PRIMARY KEY,
|
|
15516
|
+
environment TEXT NOT NULL,
|
|
15517
|
+
created_at INTEGER NOT NULL,
|
|
15518
|
+
updated_at INTEGER NOT NULL
|
|
15519
|
+
);
|
|
15520
|
+
`);
|
|
15521
|
+
}
|
|
15522
|
+
/** Whether an APNs key is configured; devices are still recorded without one. */
|
|
15523
|
+
get configured() {
|
|
15524
|
+
return Boolean((this.options.keyPath || this.options.deliver) && this.options.keyId && this.options.teamId);
|
|
15525
|
+
}
|
|
15526
|
+
register(token, environment) {
|
|
15527
|
+
if (!DEVICE_TOKEN_RE.test(token)) return false;
|
|
15528
|
+
const now2 = Date.now();
|
|
15529
|
+
this.db.prepare(`INSERT INTO aster_devices (token, environment, created_at, updated_at) VALUES (?, ?, ?, ?)
|
|
15530
|
+
ON CONFLICT(token) DO UPDATE SET environment = excluded.environment, updated_at = excluded.updated_at`).run(token.toLowerCase(), environment, now2, now2);
|
|
15531
|
+
return true;
|
|
15532
|
+
}
|
|
15533
|
+
unregister(token) {
|
|
15534
|
+
this.db.prepare("DELETE FROM aster_devices WHERE token = ?").run(token.toLowerCase());
|
|
15535
|
+
}
|
|
15536
|
+
deviceCount() {
|
|
15537
|
+
return this.db.prepare("SELECT COUNT(*) AS n FROM aster_devices").get().n;
|
|
15538
|
+
}
|
|
15539
|
+
/** Sends to every registered device; tokens Apple reports as gone are removed. Never throws. */
|
|
15540
|
+
async notify(message2) {
|
|
15541
|
+
if (!this.configured) return;
|
|
15542
|
+
const devices = this.db.prepare("SELECT token, environment FROM aster_devices").all();
|
|
15543
|
+
if (devices.length === 0) return;
|
|
15544
|
+
const payload = JSON.stringify({
|
|
15545
|
+
aps: {
|
|
15546
|
+
alert: { title: truncate2(message2.title, 80), body: truncate2(message2.body, 180) },
|
|
15547
|
+
sound: "default",
|
|
15548
|
+
"thread-id": message2.conversationId
|
|
15549
|
+
},
|
|
15550
|
+
conversationId: message2.conversationId
|
|
15551
|
+
});
|
|
15552
|
+
await Promise.all(devices.map(async (device) => {
|
|
15553
|
+
const host = device.environment === "development" ? "api.sandbox.push.apple.com" : "api.push.apple.com";
|
|
15554
|
+
try {
|
|
15555
|
+
const status = await this.send(host, device.token, payload);
|
|
15556
|
+
if (status === 410 || status === 400) this.unregister(device.token);
|
|
15557
|
+
} catch {
|
|
15558
|
+
}
|
|
15559
|
+
}));
|
|
15560
|
+
}
|
|
15561
|
+
close() {
|
|
15562
|
+
for (const session of this.sessions.values()) session.close();
|
|
15563
|
+
this.sessions.clear();
|
|
15564
|
+
this.db.close();
|
|
15565
|
+
}
|
|
15566
|
+
async send(host, token, payload) {
|
|
15567
|
+
const headers = {
|
|
15568
|
+
authorization: `bearer ${this.token()}`,
|
|
15569
|
+
"apns-topic": this.options.topic,
|
|
15570
|
+
"apns-push-type": "alert",
|
|
15571
|
+
"apns-priority": "10"
|
|
15572
|
+
};
|
|
15573
|
+
if (this.options.deliver) return this.options.deliver({ host, token, headers, payload });
|
|
15574
|
+
const session = this.session(host);
|
|
15575
|
+
return await new Promise((resolve15, reject) => {
|
|
15576
|
+
const stream = session.request({ ":method": "POST", ":path": `/3/device/${token}`, ...headers, "content-type": "application/json" });
|
|
15577
|
+
let status = 0;
|
|
15578
|
+
stream.setTimeout(15e3, () => stream.close());
|
|
15579
|
+
stream.on("response", (responseHeaders) => {
|
|
15580
|
+
status = Number(responseHeaders[":status"]) || 0;
|
|
15581
|
+
});
|
|
15582
|
+
stream.on("error", reject);
|
|
15583
|
+
stream.on("close", () => resolve15(status));
|
|
15584
|
+
stream.resume();
|
|
15585
|
+
stream.end(payload);
|
|
15586
|
+
});
|
|
15587
|
+
}
|
|
15588
|
+
session(host) {
|
|
15589
|
+
const existing = this.sessions.get(host);
|
|
15590
|
+
if (existing && !existing.closed && !existing.destroyed) return existing;
|
|
15591
|
+
const session = connect(`https://${host}`);
|
|
15592
|
+
session.on("error", () => this.sessions.delete(host));
|
|
15593
|
+
session.on("close", () => this.sessions.delete(host));
|
|
15594
|
+
session.unref();
|
|
15595
|
+
this.sessions.set(host, session);
|
|
15596
|
+
return session;
|
|
15597
|
+
}
|
|
15598
|
+
/** Provider token (ES256 JWT); Apple accepts one for up to an hour, so it is reused for 50 minutes. */
|
|
15599
|
+
token() {
|
|
15600
|
+
const now2 = Math.floor(Date.now() / 1e3);
|
|
15601
|
+
if (this.jwt && now2 - this.jwt.issuedAt < 50 * 60) return this.jwt.value;
|
|
15602
|
+
this.privateKey ??= readFileSync5(this.options.keyPath, "utf8");
|
|
15603
|
+
const encode = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
15604
|
+
const unsigned = `${encode({ alg: "ES256", kid: this.options.keyId })}.${encode({ iss: this.options.teamId, iat: now2 })}`;
|
|
15605
|
+
const signature = createSign("SHA256").update(unsigned).sign({ key: this.privateKey, dsaEncoding: "ieee-p1363" }).toString("base64url");
|
|
15606
|
+
this.jwt = { value: `${unsigned}.${signature}`, issuedAt: now2 };
|
|
15607
|
+
return this.jwt.value;
|
|
15608
|
+
}
|
|
15609
|
+
};
|
|
15610
|
+
}
|
|
15611
|
+
});
|
|
15612
|
+
|
|
15613
|
+
// src/server/aster-api.ts
|
|
15614
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
14832
15615
|
import { unlink as unlink2 } from "fs/promises";
|
|
14833
15616
|
import { resolve as resolve8 } from "path";
|
|
14834
15617
|
function parseCursor(header, query) {
|
|
@@ -14837,8 +15620,23 @@ function parseCursor(header, query) {
|
|
|
14837
15620
|
const cursor = Number(value);
|
|
14838
15621
|
return Number.isSafeInteger(cursor) ? cursor : void 0;
|
|
14839
15622
|
}
|
|
14840
|
-
function
|
|
15623
|
+
function collectAssistantText(store, conversationId, runId, after) {
|
|
15624
|
+
let text = "";
|
|
15625
|
+
let cursor = after;
|
|
15626
|
+
for (; ; ) {
|
|
15627
|
+
const events = store.eventsAfter(conversationId, cursor, 500);
|
|
15628
|
+
if (events.length === 0) break;
|
|
15629
|
+
for (const event of events) {
|
|
15630
|
+
if (event.runId === runId && event.type === "assistant.delta" && typeof event.data.text === "string") text += event.data.text;
|
|
15631
|
+
}
|
|
15632
|
+
cursor = events.at(-1).sequence;
|
|
15633
|
+
if (text.length > 2e4) break;
|
|
15634
|
+
}
|
|
15635
|
+
return text;
|
|
15636
|
+
}
|
|
15637
|
+
function publicConversation(conversation, project) {
|
|
14841
15638
|
return {
|
|
15639
|
+
...project ? { kind: project.kind, repository: project.repository, branch: project.branch } : {},
|
|
14842
15640
|
conversationId: conversation.id,
|
|
14843
15641
|
workspaceId: conversation.workspaceId,
|
|
14844
15642
|
model: conversation.model,
|
|
@@ -14862,6 +15660,25 @@ function publicApproval(approval) {
|
|
|
14862
15660
|
decision: approval.decision
|
|
14863
15661
|
};
|
|
14864
15662
|
}
|
|
15663
|
+
async function selectableModels(config2) {
|
|
15664
|
+
if (config2.modelCatalog === "fixed") return config2.models.map((id) => ({ id }));
|
|
15665
|
+
const catalog = await loadOpenRouterCatalog();
|
|
15666
|
+
return catalog.filter((model) => model.supports.tools).map((model) => ({
|
|
15667
|
+
id: model.id,
|
|
15668
|
+
...model.name ? { name: model.name } : {},
|
|
15669
|
+
contextWindow: model.contextWindow,
|
|
15670
|
+
inputPerMtok: model.pricing.inputPerMtok,
|
|
15671
|
+
outputPerMtok: model.pricing.outputPerMtok,
|
|
15672
|
+
...model.created !== void 0 ? { created: model.created } : {},
|
|
15673
|
+
reasoning: model.supports.reasoning,
|
|
15674
|
+
vision: model.supports.vision === true
|
|
15675
|
+
}));
|
|
15676
|
+
}
|
|
15677
|
+
async function isSelectableModel(config2, model) {
|
|
15678
|
+
if (!model.trim() || model.length > 200) return false;
|
|
15679
|
+
if (config2.modelCatalog === "fixed") return config2.models.includes(model);
|
|
15680
|
+
return (await selectableModels(config2)).some((entry) => entry.id === model);
|
|
15681
|
+
}
|
|
14865
15682
|
async function readJsonBody(req) {
|
|
14866
15683
|
const chunks = [];
|
|
14867
15684
|
let bytes = 0;
|
|
@@ -14903,10 +15720,15 @@ var init_aster_api = __esm({
|
|
|
14903
15720
|
init_version();
|
|
14904
15721
|
init_aster_store();
|
|
14905
15722
|
init_aster_workspaces();
|
|
15723
|
+
init_openrouter_catalog();
|
|
15724
|
+
init_aster_github();
|
|
15725
|
+
init_aster_memory();
|
|
15726
|
+
init_aster_github_proxy();
|
|
15727
|
+
init_aster_push();
|
|
14906
15728
|
init_aster_tools();
|
|
14907
15729
|
API_PREFIX = "/api/v1";
|
|
14908
|
-
MAX_BODY_BYTES =
|
|
14909
|
-
MAX_USER_TURN_CHARS =
|
|
15730
|
+
MAX_BODY_BYTES = 4 * 1024 * 1024;
|
|
15731
|
+
MAX_USER_TURN_CHARS = 5e5;
|
|
14910
15732
|
CLIENT_TURN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
14911
15733
|
CREATE_IDEMPOTENCY_KEY_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
14912
15734
|
MAX_APPROVAL_ARG_BYTES = 24 * 1024;
|
|
@@ -14918,9 +15740,57 @@ var init_aster_api = __esm({
|
|
|
14918
15740
|
waiters = /* @__PURE__ */ new Map();
|
|
14919
15741
|
runtime;
|
|
14920
15742
|
config;
|
|
14921
|
-
|
|
15743
|
+
github;
|
|
15744
|
+
memory;
|
|
15745
|
+
constructor(config2, runtime, github = new AsterGitHub(), memory) {
|
|
14922
15746
|
this.config = config2;
|
|
14923
15747
|
this.runtime = runtime;
|
|
15748
|
+
this.github = github;
|
|
15749
|
+
this.memory = memory;
|
|
15750
|
+
}
|
|
15751
|
+
githubProxy;
|
|
15752
|
+
/** Where cells reach the repository-scoped GitHub proxy, when it is running. */
|
|
15753
|
+
githubProxyUrl;
|
|
15754
|
+
/**
|
|
15755
|
+
* Starts the repository-scoped GitHub proxy when configured
|
|
15756
|
+
* (AUTOPILOT_ASTER_GITHUB_PROXY_LISTEN=host:port, AUTOPILOT_ASTER_GITHUB_PROXY_URL as cells see it).
|
|
15757
|
+
*/
|
|
15758
|
+
async startGitHubProxy(env2 = process.env) {
|
|
15759
|
+
const listen = env2.AUTOPILOT_ASTER_GITHUB_PROXY_LISTEN;
|
|
15760
|
+
const publicUrl = env2.AUTOPILOT_ASTER_GITHUB_PROXY_URL;
|
|
15761
|
+
const token = env2.GITHUB_TOKEN;
|
|
15762
|
+
if (!listen || !publicUrl || !token || this.githubProxy) return;
|
|
15763
|
+
const separator = listen.lastIndexOf(":");
|
|
15764
|
+
const proxy = new AsterGitHubProxy({ token, resolveGrant: (grant) => this.getStore().resolveGitHubGrant(grant) });
|
|
15765
|
+
await proxy.listen(Number(listen.slice(separator + 1)), listen.slice(0, separator));
|
|
15766
|
+
this.githubProxy = proxy;
|
|
15767
|
+
this.githubProxyUrl = publicUrl.replace(/\/+$/, "");
|
|
15768
|
+
}
|
|
15769
|
+
push;
|
|
15770
|
+
getPush() {
|
|
15771
|
+
try {
|
|
15772
|
+
return this.push ??= new AsterPush();
|
|
15773
|
+
} catch {
|
|
15774
|
+
return void 0;
|
|
15775
|
+
}
|
|
15776
|
+
}
|
|
15777
|
+
/** "Your task finished" (or failed) on the user's devices; cancelled turns stay silent. */
|
|
15778
|
+
notifyTurnEnd(store, conversationId, runId, status, reason, userText, after) {
|
|
15779
|
+
if (status !== "completed" && status !== "failed") return;
|
|
15780
|
+
const push = this.getPush();
|
|
15781
|
+
if (!push?.configured) return;
|
|
15782
|
+
const reply = status === "completed" ? collectAssistantText(store, conversationId, runId, after).trim() : "";
|
|
15783
|
+
const title = userText.trim().split("\n")[0] || "Aster";
|
|
15784
|
+
const body = status === "completed" ? reply ? `\u2705 ${reply}` : "\u2705 Done" : `\u26A0\uFE0F Couldn\u2019t finish${reason && reason !== "error" ? `: ${reason}` : ""}`;
|
|
15785
|
+
void push.notify({ title, body, conversationId });
|
|
15786
|
+
}
|
|
15787
|
+
getMemory() {
|
|
15788
|
+
if (process.env.AUTOPILOT_ASTER_MEMORY === "off") return void 0;
|
|
15789
|
+
try {
|
|
15790
|
+
return this.memory ??= new AsterMemory();
|
|
15791
|
+
} catch {
|
|
15792
|
+
return void 0;
|
|
15793
|
+
}
|
|
14924
15794
|
}
|
|
14925
15795
|
async handle(req, res) {
|
|
14926
15796
|
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
@@ -14942,7 +15812,7 @@ var init_aster_api = __esm({
|
|
|
14942
15812
|
service: "autopilot",
|
|
14943
15813
|
apiVersion: "v1",
|
|
14944
15814
|
serviceVersion: getAppVersion(),
|
|
14945
|
-
capabilities: ["workspaces", "conversations", "turns", "event-replay", "cancellation", "human-approvals", "hotcell-per-conversation", "pause-resume", "destroy"]
|
|
15815
|
+
capabilities: ["workspaces", "conversations", "turns", "event-replay", "cancellation", "human-approvals", "hotcell-per-conversation", "pause-resume", "destroy", "autonomous-cells", "tool-output", "checkpoints", "model-switch", "artifacts", "memory", "push", ...this.github.configured ? ["github"] : [], ...config2.modelCatalog === "openrouter" ? ["model-catalog"] : []]
|
|
14946
15816
|
});
|
|
14947
15817
|
}
|
|
14948
15818
|
if (path === `${API_PREFIX}/workspaces` && method === "GET") {
|
|
@@ -14953,27 +15823,86 @@ var init_aster_api = __esm({
|
|
|
14953
15823
|
}
|
|
14954
15824
|
if (path === `${API_PREFIX}/models` && method === "GET") {
|
|
14955
15825
|
if (!this.hasScope(principal, "models:read")) return sendError(res, 403, "insufficient_scope", "Credential lacks model/read scope");
|
|
14956
|
-
|
|
15826
|
+
const models = await selectableModels(config2);
|
|
15827
|
+
if (models.length === 0) return sendError(res, 503, "model_catalog_unavailable", "The model catalog is unavailable; try again shortly");
|
|
15828
|
+
return json(res, 200, { models: models.map((model) => model.id), catalog: models });
|
|
14957
15829
|
}
|
|
14958
15830
|
if (path === `${API_PREFIX}/conversations` && method === "POST") {
|
|
14959
15831
|
if (!this.hasScope(principal, "conversations:write")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/write scope");
|
|
14960
15832
|
const body = await readJsonBody(req);
|
|
14961
|
-
if (Object.keys(body).some((key) =>
|
|
14962
|
-
return sendError(res, 400, "unsupported_field", "Conversation creation accepts
|
|
15833
|
+
if (Object.keys(body).some((key) => !["workspaceId", "model", "kind", "repository"].includes(key))) {
|
|
15834
|
+
return sendError(res, 400, "unsupported_field", "Conversation creation accepts workspaceId, model, kind, and repository; paths are server-configured");
|
|
15835
|
+
}
|
|
15836
|
+
if (body.kind !== void 0 && body.kind !== "chat" && body.kind !== "code") {
|
|
15837
|
+
return sendError(res, 400, "invalid_kind", 'kind must be "chat" or "code"');
|
|
15838
|
+
}
|
|
15839
|
+
if (body.kind === "code" && (typeof body.repository !== "string" || !REPOSITORY_FULL_NAME_RE.test(body.repository))) {
|
|
15840
|
+
return sendError(res, 400, "repository_required", "Code conversations need a repository as owner/name");
|
|
15841
|
+
}
|
|
15842
|
+
if (body.kind !== "code" && body.repository !== void 0) {
|
|
15843
|
+
return sendError(res, 400, "unsupported_field", "repository is only accepted for code conversations");
|
|
14963
15844
|
}
|
|
14964
15845
|
if (typeof body.workspaceId !== "string" || !principal.workspaceIds.includes(body.workspaceId)) {
|
|
14965
15846
|
return sendError(res, 403, "workspace_forbidden", "Workspace ID is not in this credential's scope");
|
|
14966
15847
|
}
|
|
14967
15848
|
const workspace = findAsterWorkspace(config2, body.workspaceId);
|
|
14968
15849
|
if (!workspace) return sendError(res, 404, "workspace_not_found", "Workspace is not configured");
|
|
14969
|
-
if (typeof body.model !== "string" || !config2
|
|
15850
|
+
if (typeof body.model !== "string" || !await isSelectableModel(config2, body.model)) {
|
|
14970
15851
|
return sendError(res, 400, "model_not_allowed", "Select a model from the configured model list");
|
|
14971
15852
|
}
|
|
14972
15853
|
const idempotencyKey = req.headers["idempotency-key"];
|
|
14973
15854
|
if (typeof idempotencyKey !== "string" || !CREATE_IDEMPOTENCY_KEY_RE.test(idempotencyKey)) {
|
|
14974
15855
|
return sendError(res, 400, "idempotency_key_required", "Conversation creation requires an Idempotency-Key header of 1-128 safe characters");
|
|
14975
15856
|
}
|
|
14976
|
-
|
|
15857
|
+
const kind = body.kind;
|
|
15858
|
+
return await this.createConversation(res, store, principal, workspace, body.model, idempotencyKey, kind, body.repository);
|
|
15859
|
+
}
|
|
15860
|
+
if (path === `${API_PREFIX}/devices` && method === "POST") {
|
|
15861
|
+
if (!this.hasScope(principal, "conversations:write")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/write scope");
|
|
15862
|
+
const body = await readJsonBody(req);
|
|
15863
|
+
const environment = body.environment === "development" ? "development" : "production";
|
|
15864
|
+
const push = this.getPush();
|
|
15865
|
+
if (!push || typeof body.token !== "string" || !push.register(body.token, environment)) {
|
|
15866
|
+
return sendError(res, 400, "invalid_device_token", "A hex APNs device token is required");
|
|
15867
|
+
}
|
|
15868
|
+
return json(res, 200, { registered: true, notifications: push.configured });
|
|
15869
|
+
}
|
|
15870
|
+
const deviceMatch = path.match(/^\/api\/v1\/devices\/([0-9a-fA-F]{64,200})$/);
|
|
15871
|
+
if (deviceMatch && method === "DELETE") {
|
|
15872
|
+
if (!this.hasScope(principal, "conversations:write")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/write scope");
|
|
15873
|
+
this.getPush()?.unregister(deviceMatch[1]);
|
|
15874
|
+
return json(res, 200, { unregistered: true });
|
|
15875
|
+
}
|
|
15876
|
+
if (path === `${API_PREFIX}/memory` && method === "GET") {
|
|
15877
|
+
if (!this.hasScope(principal, "conversations:read")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/read scope");
|
|
15878
|
+
const memory = this.getMemory();
|
|
15879
|
+
return json(res, 200, { enabled: Boolean(memory?.enabled), items: memory ? memory.list() : [] });
|
|
15880
|
+
}
|
|
15881
|
+
const memoryMatch = path.match(/^\/api\/v1\/memory\/([^/]+)$/);
|
|
15882
|
+
if (memoryMatch && method === "DELETE") {
|
|
15883
|
+
if (!this.hasScope(principal, "conversations:write")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/write scope");
|
|
15884
|
+
const memory = this.getMemory();
|
|
15885
|
+
if (!memory || !memory.delete(memoryMatch[1])) return sendError(res, 404, "memory_not_found", "Memory item not found");
|
|
15886
|
+
return json(res, 200, { deleted: memoryMatch[1] });
|
|
15887
|
+
}
|
|
15888
|
+
if (path === `${API_PREFIX}/github/repositories` && (method === "GET" || method === "POST")) {
|
|
15889
|
+
if (!this.hasScope(principal, "conversations:write")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/write scope");
|
|
15890
|
+
try {
|
|
15891
|
+
if (method === "GET") {
|
|
15892
|
+
return json(res, 200, { repositories: await this.github.listRepositories(url.searchParams.get("q") ?? "") });
|
|
15893
|
+
}
|
|
15894
|
+
const body = await readJsonBody(req);
|
|
15895
|
+
if (typeof body.name !== "string") return sendError(res, 400, "invalid_repository_name", "name is required");
|
|
15896
|
+
const repository = await this.github.createRepository({
|
|
15897
|
+
name: body.name.trim(),
|
|
15898
|
+
private: body.private !== false,
|
|
15899
|
+
...typeof body.description === "string" ? { description: body.description } : {}
|
|
15900
|
+
});
|
|
15901
|
+
return json(res, 201, { repository });
|
|
15902
|
+
} catch (error) {
|
|
15903
|
+
if (error instanceof AsterGitHubError) return sendError(res, error.status, error.code, error.message);
|
|
15904
|
+
throw error;
|
|
15905
|
+
}
|
|
14977
15906
|
}
|
|
14978
15907
|
const eventsMatch = path.match(/^\/api\/v1\/conversations\/([^/]+)\/events$/);
|
|
14979
15908
|
if (eventsMatch && method === "GET") {
|
|
@@ -14990,7 +15919,7 @@ var init_aster_api = __esm({
|
|
|
14990
15919
|
const conversation = await this.authorizedConversation(store, principal, turnMatch[1]);
|
|
14991
15920
|
if (!conversation) return sendError(res, 404, "conversation_not_found", "Conversation not found");
|
|
14992
15921
|
const body = await readJsonBody(req);
|
|
14993
|
-
if (Object.keys(body).some((key) => key !== "text" && key !== "clientTurnId")) return sendError(res, 400, "unsupported_field", "Turns accept text and an optional
|
|
15922
|
+
if (Object.keys(body).some((key) => key !== "text" && key !== "clientTurnId" && key !== "model")) return sendError(res, 400, "unsupported_field", "Turns accept text, an optional clientTurnId, and an optional model; the workspace is fixed by the conversation");
|
|
14994
15923
|
const hasClientTurnId = Object.hasOwn(body, "clientTurnId");
|
|
14995
15924
|
const clientTurnId = hasClientTurnId && typeof body.clientTurnId === "string" ? body.clientTurnId : void 0;
|
|
14996
15925
|
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");
|
|
@@ -14998,7 +15927,18 @@ var init_aster_api = __esm({
|
|
|
14998
15927
|
const text = requestText.trim();
|
|
14999
15928
|
if (!text || text.length > MAX_USER_TURN_CHARS) return sendError(res, 400, "invalid_turn", `Turn text must be 1-${MAX_USER_TURN_CHARS} characters`);
|
|
15000
15929
|
if (containsLikelyProviderSecret(text)) return sendError(res, 400, "secret_input_rejected", "Provider credentials must not be sent in conversation text");
|
|
15001
|
-
|
|
15930
|
+
let target = conversation;
|
|
15931
|
+
if (Object.hasOwn(body, "model")) {
|
|
15932
|
+
if (typeof body.model !== "string" || !await isSelectableModel(config2, body.model)) {
|
|
15933
|
+
return sendError(res, 400, "model_not_allowed", "Select a model from the model catalog");
|
|
15934
|
+
}
|
|
15935
|
+
if (body.model !== conversation.model) {
|
|
15936
|
+
const switched = await this.switchModel(res, store, conversation, body.model);
|
|
15937
|
+
if (!switched) return;
|
|
15938
|
+
target = switched;
|
|
15939
|
+
}
|
|
15940
|
+
}
|
|
15941
|
+
return await this.appendTurn(res, store, target, clientTurnId, requestText, text);
|
|
15002
15942
|
}
|
|
15003
15943
|
const cancelMatch = path.match(/^\/api\/v1\/conversations\/([^/]+)\/cancel$/);
|
|
15004
15944
|
if (cancelMatch && method === "POST") {
|
|
@@ -15014,6 +15954,61 @@ var init_aster_api = __esm({
|
|
|
15014
15954
|
if (!conversation) return sendError(res, 404, "conversation_not_found", "Conversation not found");
|
|
15015
15955
|
return lifecycleMatch[2] === "pause" ? this.pauseConversation(res, store, conversation) : this.resumeConversation(res, store, conversation);
|
|
15016
15956
|
}
|
|
15957
|
+
const artifactsMatch = path.match(/^\/api\/v1\/conversations\/([^/]+)\/artifacts(\/file)?$/);
|
|
15958
|
+
if (artifactsMatch && method === "GET") {
|
|
15959
|
+
if (!this.hasScope(principal, "conversations:read")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/read scope");
|
|
15960
|
+
const conversation = await this.authorizedConversation(store, principal, artifactsMatch[1]);
|
|
15961
|
+
if (!conversation) return sendError(res, 404, "conversation_not_found", "Conversation not found");
|
|
15962
|
+
const cell = this.readyCell(res, store, conversation.id);
|
|
15963
|
+
if (!cell) return;
|
|
15964
|
+
if (!this.runtime.listArtifacts || !this.runtime.readArtifact) return sendError(res, 503, "artifacts_unavailable", "Artifacts are not configured");
|
|
15965
|
+
if (!artifactsMatch[2]) return json(res, 200, { artifacts: await this.runtime.listArtifacts(cell) });
|
|
15966
|
+
try {
|
|
15967
|
+
const artifact = await this.runtime.readArtifact(cell, url.searchParams.get("path") ?? "");
|
|
15968
|
+
res.writeHead(200, {
|
|
15969
|
+
"content-type": artifact.mimeType,
|
|
15970
|
+
"content-length": artifact.data.length,
|
|
15971
|
+
"content-disposition": `attachment; filename*=UTF-8''${encodeURIComponent(artifact.name)}`,
|
|
15972
|
+
"cache-control": "no-store",
|
|
15973
|
+
"x-content-type-options": "nosniff"
|
|
15974
|
+
});
|
|
15975
|
+
res.end(artifact.data);
|
|
15976
|
+
return;
|
|
15977
|
+
} catch (error) {
|
|
15978
|
+
const code = error instanceof Error ? error.message : "";
|
|
15979
|
+
if (code === "invalid_artifact_path") return sendError(res, 400, code, "Artifact path is invalid");
|
|
15980
|
+
if (code === "artifact_too_large") return sendError(res, 413, code, "Artifact is larger than 50 MB");
|
|
15981
|
+
return sendError(res, 404, "artifact_not_found", "Artifact not found");
|
|
15982
|
+
}
|
|
15983
|
+
}
|
|
15984
|
+
const checkpointsMatch = path.match(/^\/api\/v1\/conversations\/([^/]+)\/checkpoints$/);
|
|
15985
|
+
if (checkpointsMatch && method === "GET") {
|
|
15986
|
+
if (!this.hasScope(principal, "conversations:read")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/read scope");
|
|
15987
|
+
const conversation = await this.authorizedConversation(store, principal, checkpointsMatch[1]);
|
|
15988
|
+
if (!conversation) return sendError(res, 404, "conversation_not_found", "Conversation not found");
|
|
15989
|
+
const cell = this.readyCell(res, store, conversation.id);
|
|
15990
|
+
if (!cell) return;
|
|
15991
|
+
if (!this.runtime.listCheckpoints) return sendError(res, 503, "checkpoints_unavailable", "Checkpoints are not configured");
|
|
15992
|
+
return json(res, 200, { checkpoints: await this.runtime.listCheckpoints(cell) });
|
|
15993
|
+
}
|
|
15994
|
+
const restoreMatch = path.match(/^\/api\/v1\/conversations\/([^/]+)\/checkpoints\/([0-9a-f]{7,40})\/restore$/);
|
|
15995
|
+
if (restoreMatch && method === "POST") {
|
|
15996
|
+
if (!this.hasScope(principal, "conversations:write")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/write scope");
|
|
15997
|
+
const conversation = await this.authorizedConversation(store, principal, restoreMatch[1]);
|
|
15998
|
+
if (!conversation) return sendError(res, 404, "conversation_not_found", "Conversation not found");
|
|
15999
|
+
if (conversation.activeRunId) return sendError(res, 409, "turn_active", "Wait for the current turn to finish or cancel it before restoring");
|
|
16000
|
+
const cell = this.readyCell(res, store, conversation.id);
|
|
16001
|
+
if (!cell) return;
|
|
16002
|
+
if (!this.runtime.restoreCheckpoint) return sendError(res, 503, "checkpoints_unavailable", "Checkpoints are not configured");
|
|
16003
|
+
try {
|
|
16004
|
+
const checkpoint = await this.runtime.restoreCheckpoint(cell, conversation.id, restoreMatch[2]);
|
|
16005
|
+
store.appendEvent(conversation.id, null, "checkpoint.restored", { restoredFrom: restoreMatch[2], ...checkpoint });
|
|
16006
|
+
return json(res, 200, { checkpoint });
|
|
16007
|
+
} catch (error) {
|
|
16008
|
+
if (error instanceof Error && error.message === "checkpoint_not_found") return sendError(res, 404, "checkpoint_not_found", "Checkpoint not found");
|
|
16009
|
+
return sendError(res, 500, "checkpoint_restore_failed", "The workspace could not be restored");
|
|
16010
|
+
}
|
|
16011
|
+
}
|
|
15017
16012
|
const conversationMatch = path.match(/^\/api\/v1\/conversations\/([^/]+)$/);
|
|
15018
16013
|
if (conversationMatch && method === "DELETE") {
|
|
15019
16014
|
if (!this.hasScope(principal, "conversations:write")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/write scope");
|
|
@@ -15025,7 +16020,7 @@ var init_aster_api = __esm({
|
|
|
15025
16020
|
if (!this.hasScope(principal, "conversations:read")) return sendError(res, 403, "insufficient_scope", "Credential lacks conversation/read scope");
|
|
15026
16021
|
const conversation = await this.authorizedConversation(store, principal, conversationMatch[1]);
|
|
15027
16022
|
if (!conversation) return sendError(res, 404, "conversation_not_found", "Conversation not found");
|
|
15028
|
-
return json(res, 200, publicConversation(conversation));
|
|
16023
|
+
return json(res, 200, publicConversation(conversation, store.getConversationProject(conversation.id)));
|
|
15029
16024
|
}
|
|
15030
16025
|
const approvalMatch = path.match(/^\/api\/v1\/approvals\/([^/]+)$/);
|
|
15031
16026
|
if (approvalMatch && method === "GET") {
|
|
@@ -15064,6 +16059,12 @@ var init_aster_api = __esm({
|
|
|
15064
16059
|
}
|
|
15065
16060
|
this.store?.close();
|
|
15066
16061
|
this.store = void 0;
|
|
16062
|
+
this.memory?.close();
|
|
16063
|
+
this.memory = void 0;
|
|
16064
|
+
this.githubProxy?.close();
|
|
16065
|
+
this.githubProxy = void 0;
|
|
16066
|
+
this.push?.close();
|
|
16067
|
+
this.push = void 0;
|
|
15067
16068
|
}
|
|
15068
16069
|
getStore() {
|
|
15069
16070
|
return this.store ??= new AsterStore();
|
|
@@ -15115,17 +16116,60 @@ var init_aster_api = __esm({
|
|
|
15115
16116
|
} else if (run.status === "needs_input") {
|
|
15116
16117
|
runs.transition(run.id, "cancelled", "approval_expired_on_restart");
|
|
15117
16118
|
store.updateConversationStatus(conversation.id, run.id, "interrupted", { reason: "approval_expired_on_restart" });
|
|
16119
|
+
} else {
|
|
16120
|
+
const mapping = store.getCellMapping(conversation.id);
|
|
16121
|
+
if (mapping?.cellId && this.runtime.resumeTurn) {
|
|
16122
|
+
this.runtime.resumeTurn(this.cellTurn(store, conversation, run.id, mapping.cellId, mapping.eventCursor, run.task ?? ""));
|
|
16123
|
+
} else {
|
|
16124
|
+
runs.transition(run.id, "failed", "server_restarted");
|
|
16125
|
+
store.updateConversationStatus(conversation.id, run.id, "interrupted", { reason: "server_restart_recovery" });
|
|
16126
|
+
}
|
|
15118
16127
|
}
|
|
15119
16128
|
}
|
|
15120
16129
|
} finally {
|
|
15121
16130
|
runs.close();
|
|
15122
16131
|
}
|
|
15123
|
-
})();
|
|
15124
|
-
return this.recovery;
|
|
16132
|
+
})();
|
|
16133
|
+
return this.recovery;
|
|
16134
|
+
}
|
|
16135
|
+
/** A turn handle for re-attaching to a cell turn after a restart (no prompt is sent). */
|
|
16136
|
+
cellTurn(store, conversation, runId, cellId, cursor, userText) {
|
|
16137
|
+
const publishEvent = (type, data) => {
|
|
16138
|
+
store.appendEvent(conversation.id, runId, type, data);
|
|
16139
|
+
};
|
|
16140
|
+
return {
|
|
16141
|
+
runId,
|
|
16142
|
+
conversation,
|
|
16143
|
+
cellId,
|
|
16144
|
+
cellEventCursor: cursor,
|
|
16145
|
+
userText,
|
|
16146
|
+
sessionFile: {},
|
|
16147
|
+
messages: [],
|
|
16148
|
+
executor: new ToolExecutor([]),
|
|
16149
|
+
allowedTools: /* @__PURE__ */ new Set(),
|
|
16150
|
+
maxToolIterations: null,
|
|
16151
|
+
maxRuntimeMs: null,
|
|
16152
|
+
askPermission: async () => "deny",
|
|
16153
|
+
publishEvent,
|
|
16154
|
+
onCellCursor: (next) => store.advanceCellEventCursor(conversation.id, next),
|
|
16155
|
+
finish: (status, reason) => {
|
|
16156
|
+
const runs = new RunStore();
|
|
16157
|
+
try {
|
|
16158
|
+
const run = runs.getRun(runId);
|
|
16159
|
+
if (run && ["queued", "running", "waiting", "needs_input"].includes(run.status)) runs.transition(runId, status, reason ?? status);
|
|
16160
|
+
} catch {
|
|
16161
|
+
} finally {
|
|
16162
|
+
runs.close();
|
|
16163
|
+
}
|
|
16164
|
+
const updated = store.updateConversationStatus(conversation.id, runId, status, reason ? { reason } : {});
|
|
16165
|
+
if (updated) store.appendEvent(conversation.id, runId, status, reason ? { reason } : {});
|
|
16166
|
+
if (updated) this.notifyTurnEnd(store, conversation.id, runId, status, reason, userText, 0);
|
|
16167
|
+
}
|
|
16168
|
+
};
|
|
15125
16169
|
}
|
|
15126
|
-
async createConversation(res, store, principal, workspace, model, idempotencyKey) {
|
|
15127
|
-
const candidateId =
|
|
15128
|
-
const candidateSessionId =
|
|
16170
|
+
async createConversation(res, store, principal, workspace, model, idempotencyKey, kind, repository) {
|
|
16171
|
+
const candidateId = randomUUID7();
|
|
16172
|
+
const candidateSessionId = randomUUID7();
|
|
15129
16173
|
const reservation = store.reserveCellConversation({
|
|
15130
16174
|
conversationId: candidateId,
|
|
15131
16175
|
credentialId: principal.credentialId,
|
|
@@ -15140,12 +16184,47 @@ var init_aster_api = __esm({
|
|
|
15140
16184
|
if (mapping.status === "destroyed") return sendError(res, 410, "conversation_destroyed", "Conversation was already destroyed");
|
|
15141
16185
|
if (mapping.status === "ready") {
|
|
15142
16186
|
const existing = store.getConversation(mapping.conversationId);
|
|
15143
|
-
if (existing) return json(res, 200, publicConversation(existing));
|
|
16187
|
+
if (existing) return json(res, 200, publicConversation(existing, store.getConversationProject(existing.id)));
|
|
15144
16188
|
}
|
|
15145
16189
|
if (!this.runtime.provisionConversation) {
|
|
15146
16190
|
store.setCellStatus(mapping.conversationId, "failed");
|
|
15147
16191
|
return sendError(res, 503, "hotcell_unavailable", "Hotcell conversation runtime is not configured");
|
|
15148
16192
|
}
|
|
16193
|
+
let project;
|
|
16194
|
+
if (kind) {
|
|
16195
|
+
let stored = store.getConversationProject(mapping.conversationId);
|
|
16196
|
+
if (!stored) {
|
|
16197
|
+
if (kind === "code") {
|
|
16198
|
+
let repo;
|
|
16199
|
+
try {
|
|
16200
|
+
repo = await this.github.getRepository(repository);
|
|
16201
|
+
} catch (error) {
|
|
16202
|
+
if (error instanceof AsterGitHubError) return sendError(res, error.status, error.code, error.message);
|
|
16203
|
+
throw error;
|
|
16204
|
+
}
|
|
16205
|
+
store.setConversationProject(mapping.conversationId, {
|
|
16206
|
+
kind,
|
|
16207
|
+
repository: repo.fullName,
|
|
16208
|
+
branch: `aster/${mapping.conversationId.slice(0, 8)}`
|
|
16209
|
+
});
|
|
16210
|
+
stored = { kind, repository: repo.fullName, branch: `aster/${mapping.conversationId.slice(0, 8)}` };
|
|
16211
|
+
project = { kind, repository: repo.fullName, baseBranch: repo.defaultBranch, branch: stored.branch };
|
|
16212
|
+
} else {
|
|
16213
|
+
store.setConversationProject(mapping.conversationId, { kind, repository: null, branch: null });
|
|
16214
|
+
stored = { kind, repository: null, branch: null };
|
|
16215
|
+
}
|
|
16216
|
+
}
|
|
16217
|
+
if (!project) {
|
|
16218
|
+
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" };
|
|
16219
|
+
}
|
|
16220
|
+
if (project.kind === "code" && project.repository && this.githubProxyUrl) {
|
|
16221
|
+
project.github = {
|
|
16222
|
+
token: store.getOrCreateGitHubGrant(mapping.conversationId, project.repository),
|
|
16223
|
+
gitUrl: `${this.githubProxyUrl}/git`,
|
|
16224
|
+
apiUrl: `${this.githubProxyUrl}/api`
|
|
16225
|
+
};
|
|
16226
|
+
}
|
|
16227
|
+
}
|
|
15149
16228
|
try {
|
|
15150
16229
|
const allowCreate = !mapping.createAttempted;
|
|
15151
16230
|
if (allowCreate) store.markCellCreateAttempted(mapping.conversationId);
|
|
@@ -15157,7 +16236,8 @@ var init_aster_api = __esm({
|
|
|
15157
16236
|
workspaceRoot: workspace.rootPath,
|
|
15158
16237
|
model,
|
|
15159
16238
|
allowCreate,
|
|
15160
|
-
onCellCreated: (cellId) => store.setCellId(mapping.conversationId, cellId)
|
|
16239
|
+
onCellCreated: (cellId) => store.setCellId(mapping.conversationId, cellId),
|
|
16240
|
+
...project ? { project } : {}
|
|
15161
16241
|
});
|
|
15162
16242
|
store.setCellId(mapping.conversationId, provisioned.cellId);
|
|
15163
16243
|
const conversation = store.getConversation(mapping.conversationId) ?? store.createConversation({
|
|
@@ -15181,7 +16261,7 @@ var init_aster_api = __esm({
|
|
|
15181
16261
|
await saveSession(sessionFile);
|
|
15182
16262
|
store.setCellStatus(mapping.conversationId, "ready");
|
|
15183
16263
|
store.appendEvent(mapping.conversationId, null, "conversation.created", { conversationId: mapping.conversationId, workspaceId: workspace.id, model });
|
|
15184
|
-
json(res, reservation.kind === "created" ? 201 : 200, publicConversation(conversation));
|
|
16264
|
+
json(res, reservation.kind === "created" ? 201 : 200, publicConversation(conversation, store.getConversationProject(conversation.id)));
|
|
15185
16265
|
} catch {
|
|
15186
16266
|
const current = store.getCellMapping(mapping.conversationId);
|
|
15187
16267
|
if (current?.cellId && this.runtime.destroyCell) {
|
|
@@ -15219,7 +16299,7 @@ var init_aster_api = __esm({
|
|
|
15219
16299
|
sendError(res, 409, "conversation_cell_unavailable", "Conversation cell is not available");
|
|
15220
16300
|
return;
|
|
15221
16301
|
}
|
|
15222
|
-
const runId =
|
|
16302
|
+
const runId = randomUUID7();
|
|
15223
16303
|
if (!idempotent && !store.beginTurn(conversation.id, runId)) {
|
|
15224
16304
|
sendError(res, 409, "conversation_busy", "A turn is already active for this conversation");
|
|
15225
16305
|
return;
|
|
@@ -15296,6 +16376,7 @@ var init_aster_api = __esm({
|
|
|
15296
16376
|
const publishEvent = (type, data) => {
|
|
15297
16377
|
store.appendEvent(conversation.id, runId, type, data);
|
|
15298
16378
|
};
|
|
16379
|
+
const turnStartSequence = store.getConversation(conversation.id)?.lastEventSequence ?? 0;
|
|
15299
16380
|
const active = {
|
|
15300
16381
|
runId,
|
|
15301
16382
|
conversation,
|
|
@@ -15314,8 +16395,20 @@ var init_aster_api = __esm({
|
|
|
15314
16395
|
finish: (status, reason) => {
|
|
15315
16396
|
const updated = store.updateConversationStatus(conversation.id, runId, status, reason ? { reason } : {});
|
|
15316
16397
|
if (updated) store.appendEvent(conversation.id, runId, status, reason ? { reason } : {});
|
|
16398
|
+
if (updated) this.notifyTurnEnd(store, conversation.id, runId, status, reason, text, turnStartSequence);
|
|
15317
16399
|
}
|
|
15318
16400
|
};
|
|
16401
|
+
const memory = this.getMemory();
|
|
16402
|
+
if (memory) {
|
|
16403
|
+
active.memory = { note: memory.brief(conversation.id, text), snapshot: memory.snapshot() };
|
|
16404
|
+
const firstSequence = store.getConversation(conversation.id)?.lastEventSequence ?? 0;
|
|
16405
|
+
active.beforeFinish = async () => {
|
|
16406
|
+
const reply = collectAssistantText(store, conversation.id, runId, firstSequence);
|
|
16407
|
+
const project = store.getConversationProject(conversation.id);
|
|
16408
|
+
const changes = await memory.remember({ conversationId: conversation.id, project, userText: text, assistantText: reply });
|
|
16409
|
+
if (changes.length) publishEvent("memory.updated", { items: changes });
|
|
16410
|
+
};
|
|
16411
|
+
}
|
|
15319
16412
|
this.runtime.startTurn(active);
|
|
15320
16413
|
json(res, 202, acceptedResponse);
|
|
15321
16414
|
} catch {
|
|
@@ -15466,6 +16559,7 @@ var init_aster_api = __esm({
|
|
|
15466
16559
|
if (mapping.status === "destroyed") return json(res, 200, { conversationId: conversation.id, status: "destroyed" });
|
|
15467
16560
|
if (!this.runtime.destroyCell) return sendError(res, 503, "hotcell_destroy_unavailable", "Hotcell cleanup is not configured");
|
|
15468
16561
|
store.setCellStatus(conversation.id, "destroying");
|
|
16562
|
+
store.revokeGitHubGrant(conversation.id);
|
|
15469
16563
|
if (conversation.activeRunId) {
|
|
15470
16564
|
for (const approval of store.cancelPendingApprovals(conversation.id)) {
|
|
15471
16565
|
const waiter = this.waiters.get(approval.id);
|
|
@@ -15491,6 +16585,50 @@ var init_aster_api = __esm({
|
|
|
15491
16585
|
sendError(res, 503, "cleanup_pending", "Cell cleanup is pending; retry DELETE to revoke credentials and remove the cell");
|
|
15492
16586
|
}
|
|
15493
16587
|
}
|
|
16588
|
+
/** The conversation's cell id when it can take control requests; otherwise sends the error. */
|
|
16589
|
+
readyCell(res, store, conversationId) {
|
|
16590
|
+
const mapping = store.getCellMapping(conversationId);
|
|
16591
|
+
if (mapping?.status === "paused") {
|
|
16592
|
+
sendError(res, 409, "conversation_paused", "Conversation is paused; resume it first");
|
|
16593
|
+
return void 0;
|
|
16594
|
+
}
|
|
16595
|
+
if (!mapping?.cellId || mapping.status !== "ready") {
|
|
16596
|
+
sendError(res, 409, "conversation_cell_unavailable", "Conversation cell is not available");
|
|
16597
|
+
return void 0;
|
|
16598
|
+
}
|
|
16599
|
+
return mapping.cellId;
|
|
16600
|
+
}
|
|
16601
|
+
/** Switch the cell session first, then the record, so a failed switch leaves both on the old model. */
|
|
16602
|
+
async switchModel(res, store, conversation, model) {
|
|
16603
|
+
if (conversation.activeRunId) {
|
|
16604
|
+
sendError(res, 409, "conversation_busy", "A turn is already active for this conversation");
|
|
16605
|
+
return void 0;
|
|
16606
|
+
}
|
|
16607
|
+
const cell = this.readyCell(res, store, conversation.id);
|
|
16608
|
+
if (!cell) return void 0;
|
|
16609
|
+
if (!this.runtime.setModel) {
|
|
16610
|
+
sendError(res, 503, "model_switch_unavailable", "Model switching is not configured");
|
|
16611
|
+
return void 0;
|
|
16612
|
+
}
|
|
16613
|
+
try {
|
|
16614
|
+
await this.runtime.setModel(cell, model);
|
|
16615
|
+
} catch {
|
|
16616
|
+
sendError(res, 503, "model_switch_failed", "The conversation's model could not be switched");
|
|
16617
|
+
return void 0;
|
|
16618
|
+
}
|
|
16619
|
+
if (!store.setConversationModel(conversation.id, model)) {
|
|
16620
|
+
sendError(res, 409, "conversation_busy", "A turn is already active for this conversation");
|
|
16621
|
+
return void 0;
|
|
16622
|
+
}
|
|
16623
|
+
try {
|
|
16624
|
+
const sessionPath = resolve8(sessionsDir(), conversation.sessionId + ".json");
|
|
16625
|
+
const sessionFile = await loadSession(sessionPath);
|
|
16626
|
+
sessionFile.model = model;
|
|
16627
|
+
await saveSession(sessionFile);
|
|
16628
|
+
} catch {
|
|
16629
|
+
}
|
|
16630
|
+
return store.getConversation(conversation.id);
|
|
16631
|
+
}
|
|
15494
16632
|
async cancelConversation(res, store, conversation) {
|
|
15495
16633
|
const runId = conversation.activeRunId;
|
|
15496
16634
|
if (!runId) {
|
|
@@ -15611,7 +16749,7 @@ data: ${JSON.stringify(event.data)}
|
|
|
15611
16749
|
// src/server/aster-cell-runtime.ts
|
|
15612
16750
|
import { execFile as execFile5 } from "child_process";
|
|
15613
16751
|
import { promisify as promisify4 } from "util";
|
|
15614
|
-
import { randomUUID as
|
|
16752
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
15615
16753
|
async function defaultArchiveWorkspace(workspaceRoot) {
|
|
15616
16754
|
const { stdout } = await execFileAsync3("git", ["-C", workspaceRoot, "archive", "--format=tar", "HEAD"], {
|
|
15617
16755
|
encoding: "buffer",
|
|
@@ -15619,17 +16757,73 @@ async function defaultArchiveWorkspace(workspaceRoot) {
|
|
|
15619
16757
|
});
|
|
15620
16758
|
return Buffer.from(stdout).toString("base64");
|
|
15621
16759
|
}
|
|
16760
|
+
function projectNote(project) {
|
|
16761
|
+
const artifacts = "Save anything meant for the user to look at (documents, slides, PDFs, spreadsheets, web pages, images, reports) in /workspace/artifacts/. Files there appear in the user's Aster app (an iPhone), where they can preview, download, and share them. Unless the user asks for an editable format, deliver visual documents (slides, reports, one-pagers) as PDF: write HTML/CSS (one page per slide, e.g. @page { size: 1280px 720px; margin: 0 }) and print it with `chromium --headless --no-sandbox --disable-gpu --no-pdf-header-footer --print-to-pdf=/workspace/artifacts/NAME.pdf FILE.html`. For editable files use python-pptx, python-docx, or openpyxl; matplotlib is available for charts. Keep drafts and source files outside artifacts/ so only finished deliverables appear there.";
|
|
16762
|
+
if (project.kind === "chat") {
|
|
16763
|
+
return `[Aster chat. /workspace is this conversation's private Linux sandbox with internet access. ${artifacts}]`;
|
|
16764
|
+
}
|
|
16765
|
+
const { repository, branch, baseBranch } = project;
|
|
16766
|
+
return `[Aster code chat. GitHub repository ${repository} is cloned at /workspace on branch ${branch} (from ${baseBranch}). Commit your work and push with \`git push -u origin HEAD\`; credentials are preconfigured, so never print or write tokens. Only this repository is reachable from here. To open a pull request: curl -s -X POST "\${ASTER_GITHUB_API_URL:-$GITHUB_BASE_URL}/repos/${repository}/pulls" -H "Authorization: Bearer \${ASTER_GITHUB_TOKEN:-$GITHUB_API_KEY}" -H "Accept: application/vnd.github+json" -d '{"title":"\u2026","head":"${branch}","base":"${baseBranch}","body":"\u2026"}'. ${artifacts} The artifacts folder is not committed to the repository.]`;
|
|
16767
|
+
}
|
|
16768
|
+
function isSafeArtifactPath(path) {
|
|
16769
|
+
if (!path || path.length > 500 || path.startsWith("/") || /[\x00-\x1f\x7f]/.test(path)) return false;
|
|
16770
|
+
return path.split("/").every((part) => part !== "" && part !== "." && part !== ".." && !part.startsWith("."));
|
|
16771
|
+
}
|
|
16772
|
+
function shellQuote2(value) {
|
|
16773
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
16774
|
+
}
|
|
16775
|
+
function artifactMimeType(path) {
|
|
16776
|
+
const extension = path.split(".").pop()?.toLowerCase() ?? "";
|
|
16777
|
+
return ARTIFACT_MIME_TYPES[extension] ?? "application/octet-stream";
|
|
16778
|
+
}
|
|
16779
|
+
function checkpointMessage(userText) {
|
|
16780
|
+
const firstLine2 = redactLikelySecrets(userText).split("\n").find((line) => line.trim())?.trim() ?? "Turn";
|
|
16781
|
+
const subject = firstLine2.length > 72 ? firstLine2.slice(0, 71) + "\u2026" : firstLine2;
|
|
16782
|
+
return `${subject}
|
|
16783
|
+
`;
|
|
16784
|
+
}
|
|
16785
|
+
function summarizeToolInput(toolName, args) {
|
|
16786
|
+
const record = args && typeof args === "object" ? args : {};
|
|
16787
|
+
const preferred = toolName === "bash" ? record.command : record.path ?? record.pattern ?? record.url ?? record.query;
|
|
16788
|
+
const text = typeof preferred === "string" ? preferred : JSON.stringify(args ?? {});
|
|
16789
|
+
return boundedToolText(text, MAX_TOOL_INPUT_CHARS);
|
|
16790
|
+
}
|
|
16791
|
+
function boundedToolText(text, limit) {
|
|
16792
|
+
const redacted = redactLikelySecrets(text);
|
|
16793
|
+
if (redacted.length <= limit) return redacted;
|
|
16794
|
+
const half = Math.floor((limit - 40) / 2);
|
|
16795
|
+
return `${redacted.slice(0, half)}
|
|
16796
|
+
\u2026 [${redacted.length - 2 * half} characters omitted] \u2026
|
|
16797
|
+
${redacted.slice(-half)}`;
|
|
16798
|
+
}
|
|
15622
16799
|
function sleep4(ms) {
|
|
15623
16800
|
return new Promise((resolve15) => setTimeout(resolve15, ms));
|
|
15624
16801
|
}
|
|
15625
|
-
var execFileAsync3, CELL_WORKSPACE, CELL_STATE_DIR, CELL_CONTROL_DIR, BRIDGE_MARKER, CONTROL_RUNNER, BRIDGE_RUNNER, HEALTH_TIMEOUT_MS, EVENT_POLL_MS, AsterHotcellRuntime;
|
|
16802
|
+
var execFileAsync3, CELL_WORKSPACE, CELL_STATE_DIR, CHECKPOINT_MESSAGE_FILE, INSTALLED_RUNTIME_PACKAGE, NOTES_FILE, MEMORY_FILE, MEMORY_EXTRACTION_TIMEOUT_MS, LIVENESS_CHECK_MS, ARTIFACTS_DIR, MAX_ARTIFACTS, MAX_ARTIFACT_BYTES, BRANCH_RE, REPOSITORY_RE, GIT, COMMIT_SHA_RE, MAX_TOOL_OUTPUT_CHARS, MAX_TOOL_INPUT_CHARS, MAX_CHECKPOINTS, CELL_CONTROL_DIR, BRIDGE_MARKER, CONTROL_RUNNER, BRIDGE_RUNNER, HEALTH_TIMEOUT_MS, EVENT_POLL_MS, AsterHotcellRuntime, ARTIFACT_MIME_TYPES;
|
|
15626
16803
|
var init_aster_cell_runtime = __esm({
|
|
15627
16804
|
"src/server/aster-cell-runtime.ts"() {
|
|
15628
16805
|
"use strict";
|
|
15629
16806
|
init_version();
|
|
16807
|
+
init_aster_tools();
|
|
15630
16808
|
execFileAsync3 = promisify4(execFile5);
|
|
15631
16809
|
CELL_WORKSPACE = "/workspace";
|
|
15632
16810
|
CELL_STATE_DIR = "/workspace/.aster";
|
|
16811
|
+
CHECKPOINT_MESSAGE_FILE = `${CELL_STATE_DIR}/checkpoint-message`;
|
|
16812
|
+
INSTALLED_RUNTIME_PACKAGE = "/opt/autopilot/node_modules/autopilot-ai/package.json";
|
|
16813
|
+
NOTES_FILE = `${CELL_STATE_DIR}/pending-notes`;
|
|
16814
|
+
MEMORY_FILE = `${CELL_STATE_DIR}/memory.md`;
|
|
16815
|
+
MEMORY_EXTRACTION_TIMEOUT_MS = 15e3;
|
|
16816
|
+
LIVENESS_CHECK_MS = 2e4;
|
|
16817
|
+
ARTIFACTS_DIR = `${CELL_WORKSPACE}/artifacts`;
|
|
16818
|
+
MAX_ARTIFACTS = 500;
|
|
16819
|
+
MAX_ARTIFACT_BYTES = 50 * 1024 * 1024;
|
|
16820
|
+
BRANCH_RE = /^[A-Za-z0-9._/-]{1,200}$/;
|
|
16821
|
+
REPOSITORY_RE = /^[A-Za-z0-9-]{1,39}\/[A-Za-z0-9._-]{1,100}$/;
|
|
16822
|
+
GIT = `git -C ${CELL_WORKSPACE} -c user.name=Autopilot -c user.email=autopilot@aster.invalid -c commit.gpgsign=false`;
|
|
16823
|
+
COMMIT_SHA_RE = /^[0-9a-f]{7,40}$/;
|
|
16824
|
+
MAX_TOOL_OUTPUT_CHARS = 16e3;
|
|
16825
|
+
MAX_TOOL_INPUT_CHARS = 2e3;
|
|
16826
|
+
MAX_CHECKPOINTS = 100;
|
|
15633
16827
|
CELL_CONTROL_DIR = `${CELL_STATE_DIR}/control`;
|
|
15634
16828
|
BRIDGE_MARKER = "startAsterCellBridge";
|
|
15635
16829
|
CONTROL_RUNNER = `node --input-type=module -e 'import { executeAsterCellControl } from "/opt/autopilot/node_modules/autopilot-ai/dist/sdk/index.js";const r = await executeAsterCellControl(process.argv[1]);process.stdout.write(JSON.stringify(r) + "\\n");'`;
|
|
@@ -15643,6 +16837,14 @@ var init_aster_cell_runtime = __esm({
|
|
|
15643
16837
|
this.archiveWorkspace = options.archiveWorkspace ?? defaultArchiveWorkspace;
|
|
15644
16838
|
this.pollIntervalMs = options.pollIntervalMs ?? EVENT_POLL_MS;
|
|
15645
16839
|
this.healthTimeoutMs = options.healthTimeoutMs ?? HEALTH_TIMEOUT_MS;
|
|
16840
|
+
this.idlePauseMs = options.idlePauseMs ?? 0;
|
|
16841
|
+
if (this.idlePauseMs > 0) {
|
|
16842
|
+
const timer3 = setInterval(() => {
|
|
16843
|
+
void this.pauseIdleCells(this.idlePauseMs).catch(() => {
|
|
16844
|
+
});
|
|
16845
|
+
}, options.idleSweepIntervalMs ?? 6e4);
|
|
16846
|
+
timer3.unref();
|
|
16847
|
+
}
|
|
15646
16848
|
}
|
|
15647
16849
|
provider;
|
|
15648
16850
|
packageSpec;
|
|
@@ -15650,22 +16852,63 @@ var init_aster_cell_runtime = __esm({
|
|
|
15650
16852
|
pollIntervalMs;
|
|
15651
16853
|
healthTimeoutMs;
|
|
15652
16854
|
activeRuns = /* @__PURE__ */ new Map();
|
|
16855
|
+
idlePauseMs;
|
|
16856
|
+
/**
|
|
16857
|
+
* Pause running cells that have no active turn and no API activity within `idleMs`,
|
|
16858
|
+
* least recently used first, stopping after `limit` pauses. Returns the paused ids.
|
|
16859
|
+
*/
|
|
16860
|
+
async pauseIdleCells(idleMs, limit = Infinity) {
|
|
16861
|
+
const busy = new Set([...this.activeRuns.values()].map((run) => run.cellId));
|
|
16862
|
+
const now2 = Date.now();
|
|
16863
|
+
const candidates = (await this.provider.listConversationCells()).filter((cell) => cell.status === "running" && !busy.has(cell.id) && now2 - Date.parse(cell.lastActivityAt) >= idleMs).sort((a, b) => Date.parse(a.lastActivityAt) - Date.parse(b.lastActivityAt));
|
|
16864
|
+
const paused = [];
|
|
16865
|
+
for (const cell of candidates) {
|
|
16866
|
+
if (paused.length >= limit) break;
|
|
16867
|
+
try {
|
|
16868
|
+
await this.provider.pauseConversationCell(cell.id);
|
|
16869
|
+
paused.push(cell.id);
|
|
16870
|
+
} catch {
|
|
16871
|
+
}
|
|
16872
|
+
}
|
|
16873
|
+
return paused;
|
|
16874
|
+
}
|
|
15653
16875
|
async provisionConversation(input) {
|
|
15654
16876
|
let cell = await this.provider.findConversationCell(input.conversationId);
|
|
15655
16877
|
if (!cell) {
|
|
15656
16878
|
if (!input.allowCreate) throw new Error("hotcell_create_uncertain");
|
|
15657
|
-
|
|
16879
|
+
const create = () => this.provider.createConversationCell({
|
|
15658
16880
|
conversationId: input.conversationId,
|
|
15659
16881
|
workspaceId: input.workspaceId,
|
|
15660
|
-
model: input.model
|
|
16882
|
+
model: input.model,
|
|
16883
|
+
...input.project ? { kind: input.project.kind } : {},
|
|
16884
|
+
...input.project?.github ? {
|
|
16885
|
+
githubViaProxy: true,
|
|
16886
|
+
env: {
|
|
16887
|
+
ASTER_GITHUB_TOKEN: input.project.github.token,
|
|
16888
|
+
ASTER_GITHUB_GIT_URL: input.project.github.gitUrl,
|
|
16889
|
+
ASTER_GITHUB_API_URL: input.project.github.apiUrl
|
|
16890
|
+
}
|
|
16891
|
+
} : {}
|
|
15661
16892
|
});
|
|
16893
|
+
try {
|
|
16894
|
+
cell = await create();
|
|
16895
|
+
} catch (error) {
|
|
16896
|
+
if ((await this.pauseIdleCells(6e4, 1).catch(() => [])).length === 0) throw error;
|
|
16897
|
+
cell = await create();
|
|
16898
|
+
}
|
|
15662
16899
|
}
|
|
15663
16900
|
input.onCellCreated(cell.getInfo().id);
|
|
15664
|
-
|
|
16901
|
+
if (input.project?.kind === "code") {
|
|
16902
|
+
await this.cloneRepository(cell, input.project);
|
|
16903
|
+
} else if (input.project?.kind !== "chat") {
|
|
16904
|
+
await this.seedWorkspace(cell, input.workspaceRoot);
|
|
16905
|
+
}
|
|
16906
|
+
await this.ensureRepository(cell, input.project?.kind === "code");
|
|
16907
|
+
if (input.project) await this.addNote(cell, projectNote(input.project));
|
|
15665
16908
|
await this.installRuntime(cell);
|
|
15666
16909
|
await this.ensureBridge(cell);
|
|
15667
16910
|
await this.control(cell, "POST", "/rpc", {
|
|
15668
|
-
id:
|
|
16911
|
+
id: randomUUID8(),
|
|
15669
16912
|
type: "new_session",
|
|
15670
16913
|
sessionId: input.sessionId,
|
|
15671
16914
|
config: { model: input.model, cwd: CELL_WORKSPACE }
|
|
@@ -15681,19 +16924,76 @@ var init_aster_cell_runtime = __esm({
|
|
|
15681
16924
|
async resumeCell(cellId) {
|
|
15682
16925
|
await this.provider.resumeConversationCell(cellId);
|
|
15683
16926
|
}
|
|
16927
|
+
/** Switch the cell session's model between turns. The bridge persists it for session restore. */
|
|
16928
|
+
async setModel(cellId, model) {
|
|
16929
|
+
await this.ensureReady(cellId);
|
|
16930
|
+
await this.controlById(cellId, "POST", "/rpc", { id: randomUUID8(), type: "set_model", modelId: model });
|
|
16931
|
+
}
|
|
16932
|
+
/** Reinstall the runtime if the cell's image lost it and restart the bridge if it isn't running. */
|
|
16933
|
+
async ensureReady(cellId) {
|
|
16934
|
+
const cell = await this.provider.getCell(cellId);
|
|
16935
|
+
await this.installRuntime(cell);
|
|
16936
|
+
await this.ensureBridge(cell);
|
|
16937
|
+
}
|
|
16938
|
+
/** Newest-first git history of the cell workspace; one commit per completed turn. */
|
|
16939
|
+
async listCheckpoints(cellId) {
|
|
16940
|
+
const cell = await this.provider.getCell(cellId);
|
|
16941
|
+
if (!await this.ensureRepository(cell)) return [];
|
|
16942
|
+
const result2 = await cell.exec(`${GIT} log -n ${MAX_CHECKPOINTS} --format=%H%x1f%cI%x1f%s`);
|
|
16943
|
+
if (result2.exitCode !== 0) return [];
|
|
16944
|
+
return result2.stdout.split("\n").filter(Boolean).map((line) => {
|
|
16945
|
+
const [id = "", createdAt = "", message2 = ""] = line.split("");
|
|
16946
|
+
return { id, createdAt, message: message2 };
|
|
16947
|
+
});
|
|
16948
|
+
}
|
|
16949
|
+
/**
|
|
16950
|
+
* Make the workspace match a checkpoint, recorded as a new commit so the restore
|
|
16951
|
+
* itself can be undone. Ignored files (node_modules, .aster state) are untouched.
|
|
16952
|
+
*/
|
|
16953
|
+
async restoreCheckpoint(cellId, conversationId, checkpointId) {
|
|
16954
|
+
if (!COMMIT_SHA_RE.test(checkpointId)) throw new Error("invalid_checkpoint");
|
|
16955
|
+
const cell = await this.provider.getCell(cellId);
|
|
16956
|
+
if (!await this.ensureRepository(cell)) throw new Error("checkpoints_unavailable");
|
|
16957
|
+
const exists = await cell.exec(`${GIT} cat-file -e ${checkpointId}^{commit}`);
|
|
16958
|
+
if (exists.exitCode !== 0) throw new Error("checkpoint_not_found");
|
|
16959
|
+
const short = checkpointId.slice(0, 7);
|
|
16960
|
+
await cell.writeFile(CHECKPOINT_MESSAGE_FILE, `Restore checkpoint ${short}
|
|
16961
|
+
`);
|
|
16962
|
+
const restore = await cell.exec(
|
|
16963
|
+
`${GIT} restore --source=${checkpointId} --staged --worktree -- :/ && ${GIT} clean -fdq && ${GIT} commit -q --allow-empty -F ${CHECKPOINT_MESSAGE_FILE}`
|
|
16964
|
+
);
|
|
16965
|
+
if (restore.exitCode !== 0) throw new Error("checkpoint_restore_failed");
|
|
16966
|
+
void conversationId;
|
|
16967
|
+
await this.addNote(
|
|
16968
|
+
cell,
|
|
16969
|
+
`[The user restored the workspace to checkpoint ${short}. Files changed after that point were reverted; re-read files before relying on earlier observations.]`
|
|
16970
|
+
);
|
|
16971
|
+
const [head] = await this.listCheckpoints(cellId);
|
|
16972
|
+
if (!head) throw new Error("checkpoint_restore_failed");
|
|
16973
|
+
return head;
|
|
16974
|
+
}
|
|
15684
16975
|
startTurn(turn) {
|
|
15685
16976
|
void this.runTurn(turn).catch(() => {
|
|
15686
16977
|
turn.finish("failed", "cell_runtime_error");
|
|
15687
16978
|
});
|
|
15688
16979
|
}
|
|
16980
|
+
/**
|
|
16981
|
+
* Re-attach to a turn that was running when the server restarted. The cell kept working
|
|
16982
|
+
* and journaled its events, so this only resumes reading them from the saved cursor.
|
|
16983
|
+
*/
|
|
16984
|
+
resumeTurn(turn) {
|
|
16985
|
+
void this.runTurn(turn, { resume: true }).catch(() => {
|
|
16986
|
+
turn.finish("failed", "cell_runtime_error");
|
|
16987
|
+
});
|
|
16988
|
+
}
|
|
15689
16989
|
cancelRun(runId) {
|
|
15690
16990
|
const active = this.activeRuns.get(runId);
|
|
15691
16991
|
if (!active) return;
|
|
15692
16992
|
active.stop();
|
|
15693
|
-
void this.controlById(active.cellId, "POST", "/rpc", { id:
|
|
16993
|
+
void this.controlById(active.cellId, "POST", "/rpc", { id: randomUUID8(), type: "abort" }).catch(() => {
|
|
15694
16994
|
});
|
|
15695
16995
|
}
|
|
15696
|
-
async runTurn(turn) {
|
|
16996
|
+
async runTurn(turn, options = {}) {
|
|
15697
16997
|
const { cellId, runId } = turn;
|
|
15698
16998
|
let cursor = turn.cellEventCursor;
|
|
15699
16999
|
let stopped = false;
|
|
@@ -15708,12 +17008,32 @@ var init_aster_cell_runtime = __esm({
|
|
|
15708
17008
|
this.activeRuns.delete(runId);
|
|
15709
17009
|
turn.finish(status, reason);
|
|
15710
17010
|
};
|
|
17011
|
+
let outcome;
|
|
17012
|
+
let artifactsBefore = [];
|
|
15711
17013
|
try {
|
|
15712
|
-
|
|
15713
|
-
|
|
15714
|
-
|
|
15715
|
-
|
|
17014
|
+
if (options.resume) throw new Error("resume");
|
|
17015
|
+
await this.ensureReady(cellId);
|
|
17016
|
+
const cell = await this.provider.getCell(cellId);
|
|
17017
|
+
if (turn.memory) {
|
|
17018
|
+
await cell.mkdir(CELL_STATE_DIR, { parents: true });
|
|
17019
|
+
await cell.writeFile(MEMORY_FILE, turn.memory.snapshot, { mode: "0600" });
|
|
17020
|
+
}
|
|
17021
|
+
const note = [await this.readNotes(cell), turn.memory?.note ?? ""].filter(Boolean).join("\n\n");
|
|
17022
|
+
const message2 = note ? `${note}
|
|
17023
|
+
|
|
17024
|
+
${turn.userText}` : turn.userText;
|
|
17025
|
+
artifactsBefore = await this.listArtifacts(cellId).catch(() => []);
|
|
17026
|
+
await this.controlById(cellId, "POST", "/rpc", { id: runId, type: "prompt", message: message2 });
|
|
17027
|
+
if (note) await this.clearNotes(cell);
|
|
17028
|
+
} catch (error) {
|
|
17029
|
+
if (!(error instanceof Error && error.message === "resume")) {
|
|
17030
|
+
finish("failed", "cell_prompt_rejected");
|
|
17031
|
+
return;
|
|
17032
|
+
}
|
|
17033
|
+
artifactsBefore = await this.listArtifacts(cellId).catch(() => []);
|
|
15716
17034
|
}
|
|
17035
|
+
let quietPolls = 0;
|
|
17036
|
+
const quietLimit = Math.max(5, Math.ceil(LIVENESS_CHECK_MS / this.pollIntervalMs));
|
|
15717
17037
|
while (!stopped) {
|
|
15718
17038
|
await sleep4(this.pollIntervalMs);
|
|
15719
17039
|
if (stopped) break;
|
|
@@ -15724,6 +17044,18 @@ var init_aster_cell_runtime = __esm({
|
|
|
15724
17044
|
} catch {
|
|
15725
17045
|
continue;
|
|
15726
17046
|
}
|
|
17047
|
+
if (events.length === 0) {
|
|
17048
|
+
if (++quietPolls >= quietLimit) {
|
|
17049
|
+
quietPolls = 0;
|
|
17050
|
+
const status = await this.controlById(cellId, "GET", "/status").catch(() => void 0);
|
|
17051
|
+
if (status && !status.activeRunId) {
|
|
17052
|
+
outcome = { status: "failed", reason: "cell_turn_lost" };
|
|
17053
|
+
break;
|
|
17054
|
+
}
|
|
17055
|
+
}
|
|
17056
|
+
continue;
|
|
17057
|
+
}
|
|
17058
|
+
quietPolls = 0;
|
|
15727
17059
|
for (const entry of events) {
|
|
15728
17060
|
cursor = Math.max(cursor, entry.cursor);
|
|
15729
17061
|
turn.onCellCursor?.(cursor);
|
|
@@ -15736,35 +17068,72 @@ var init_aster_cell_runtime = __esm({
|
|
|
15736
17068
|
if (typeof event.text === "string") turn.publishEvent("assistant.reasoning", { text: event.text });
|
|
15737
17069
|
break;
|
|
15738
17070
|
case "tool.start":
|
|
15739
|
-
turn.publishEvent("tool.activity", {
|
|
17071
|
+
turn.publishEvent("tool.activity", {
|
|
17072
|
+
tool: event.toolName,
|
|
17073
|
+
toolCallId: event.toolCallId,
|
|
17074
|
+
activity: "started",
|
|
17075
|
+
input: summarizeToolInput(event.toolName, event.args)
|
|
17076
|
+
});
|
|
17077
|
+
break;
|
|
17078
|
+
case "tool.result":
|
|
17079
|
+
turn.publishEvent("tool.activity", {
|
|
17080
|
+
tool: event.toolName,
|
|
17081
|
+
toolCallId: event.toolCallId,
|
|
17082
|
+
activity: event.isError === true ? "failed" : "completed",
|
|
17083
|
+
output: boundedToolText(typeof event.result === "string" ? event.result : "", MAX_TOOL_OUTPUT_CHARS)
|
|
17084
|
+
});
|
|
15740
17085
|
break;
|
|
15741
17086
|
case "permission.request": {
|
|
15742
|
-
const
|
|
15743
|
-
tool: { name: event.
|
|
17087
|
+
const decision = await turn.askPermission({
|
|
17088
|
+
tool: { name: event.toolName },
|
|
15744
17089
|
args: event.args ?? {}
|
|
15745
17090
|
});
|
|
15746
17091
|
await this.controlById(cellId, "POST", "/rpc", {
|
|
15747
|
-
id:
|
|
17092
|
+
id: randomUUID8(),
|
|
15748
17093
|
type: "resolve_permission",
|
|
15749
|
-
|
|
15750
|
-
|
|
17094
|
+
requestId: event.requestId,
|
|
17095
|
+
decision
|
|
15751
17096
|
}).catch(() => {
|
|
15752
17097
|
});
|
|
15753
|
-
turn.publishEvent("tool.activity", { tool: event.
|
|
17098
|
+
turn.publishEvent("tool.activity", { tool: event.toolName, activity: decision === "allow" ? "approved" : "rejected" });
|
|
15754
17099
|
break;
|
|
15755
17100
|
}
|
|
15756
17101
|
case "usage":
|
|
15757
17102
|
turn.publishEvent("usage", { usage: event.usage });
|
|
15758
17103
|
break;
|
|
15759
17104
|
case "session.end":
|
|
15760
|
-
|
|
17105
|
+
outcome = {
|
|
17106
|
+
status: event.reason === "aborted" ? "cancelled" : event.reason === "complete" ? "completed" : "failed",
|
|
17107
|
+
// Surface the agent's actual error (redacted, bounded) instead of a bare "error".
|
|
17108
|
+
reason: event.reason === "error" && typeof event.error === "string" && event.error.trim() ? boundedToolText(event.error.trim(), 500) : String(event.reason ?? "cell_session_ended")
|
|
17109
|
+
};
|
|
17110
|
+
stopped = true;
|
|
15761
17111
|
break;
|
|
15762
17112
|
default:
|
|
15763
17113
|
break;
|
|
15764
17114
|
}
|
|
17115
|
+
if (outcome) break;
|
|
15765
17116
|
}
|
|
15766
17117
|
}
|
|
15767
|
-
|
|
17118
|
+
try {
|
|
17119
|
+
const before = new Map(artifactsBefore.map((artifact) => [artifact.path, artifact]));
|
|
17120
|
+
for (const artifact of await this.listArtifacts(cellId)) {
|
|
17121
|
+
const previous = before.get(artifact.path);
|
|
17122
|
+
if (!previous || previous.size !== artifact.size || previous.modifiedAt !== artifact.modifiedAt) {
|
|
17123
|
+
turn.publishEvent("artifact.updated", { ...artifact });
|
|
17124
|
+
}
|
|
17125
|
+
}
|
|
17126
|
+
} catch {
|
|
17127
|
+
}
|
|
17128
|
+
await this.checkpoint(cellId, turn.userText).then((checkpoint) => {
|
|
17129
|
+
if (checkpoint) turn.publishEvent("checkpoint.created", { ...checkpoint });
|
|
17130
|
+
}).catch(() => {
|
|
17131
|
+
});
|
|
17132
|
+
if (outcome?.status === "completed" && turn.beforeFinish) {
|
|
17133
|
+
await Promise.race([turn.beforeFinish().catch(() => {
|
|
17134
|
+
}), sleep4(MEMORY_EXTRACTION_TIMEOUT_MS)]);
|
|
17135
|
+
}
|
|
17136
|
+
finish(outcome?.status ?? "cancelled", outcome?.reason ?? "cell_turn_stopped");
|
|
15768
17137
|
}
|
|
15769
17138
|
async seedWorkspace(cell, workspaceRoot) {
|
|
15770
17139
|
const seeded = await cell.exec(`test -f ${CELL_STATE_DIR}/.seeded`);
|
|
@@ -15776,15 +17145,112 @@ var init_aster_cell_runtime = __esm({
|
|
|
15776
17145
|
if (extract.exitCode !== 0) throw new Error("workspace_seed_failed");
|
|
15777
17146
|
await cell.exec(`rm -f ${CELL_STATE_DIR}/workspace.tar.b64 && touch ${CELL_STATE_DIR}/.seeded`);
|
|
15778
17147
|
}
|
|
17148
|
+
/**
|
|
17149
|
+
* Make /workspace a git repository with an initial commit (idempotent), excluding the
|
|
17150
|
+
* bridge's private .aster state. Returns false when the cell image has no git, in
|
|
17151
|
+
* which case checkpoints are skipped rather than failing the conversation.
|
|
17152
|
+
*/
|
|
17153
|
+
async ensureRepository(cell, excludeArtifacts = false) {
|
|
17154
|
+
const result2 = await cell.exec(
|
|
17155
|
+
`command -v git >/dev/null || exit 3; if [ ! -d ${CELL_WORKSPACE}/.git ]; then git init -q ${CELL_WORKSPACE} || exit 1; fi; grep -qx '.aster/' ${CELL_WORKSPACE}/.git/info/exclude 2>/dev/null || printf '.aster/\\n' >> ${CELL_WORKSPACE}/.git/info/exclude; ` + // A code cell's artifacts are for the user, not the repository.
|
|
17156
|
+
(excludeArtifacts ? `grep -qx 'artifacts/' ${CELL_WORKSPACE}/.git/info/exclude || printf 'artifacts/\\n' >> ${CELL_WORKSPACE}/.git/info/exclude; ` : "") + `${GIT} rev-parse -q --verify HEAD >/dev/null || { ${GIT} add -A && ${GIT} commit -q --allow-empty -m 'Workspace created'; }`
|
|
17157
|
+
);
|
|
17158
|
+
return result2.exitCode === 0;
|
|
17159
|
+
}
|
|
17160
|
+
/** Commit all workspace changes; returns the new checkpoint, or undefined when nothing changed. */
|
|
17161
|
+
async checkpoint(cellId, userText) {
|
|
17162
|
+
const cell = await this.provider.getCell(cellId);
|
|
17163
|
+
if (!await this.ensureRepository(cell)) return void 0;
|
|
17164
|
+
await cell.writeFile(CHECKPOINT_MESSAGE_FILE, checkpointMessage(userText), { mode: "0600" });
|
|
17165
|
+
const commit = await cell.exec(
|
|
17166
|
+
`${GIT} add -A && if ${GIT} diff --cached --quiet; then exit 4; fi && ${GIT} commit -q -F ${CHECKPOINT_MESSAGE_FILE}`
|
|
17167
|
+
);
|
|
17168
|
+
if (commit.exitCode !== 0) return void 0;
|
|
17169
|
+
const [head] = await this.listCheckpoints(cellId);
|
|
17170
|
+
return head;
|
|
17171
|
+
}
|
|
17172
|
+
/**
|
|
17173
|
+
* Install the Autopilot runtime unless the cell already has the expected version. This
|
|
17174
|
+
* checks the installed package itself rather than a marker on the persistent volume:
|
|
17175
|
+
* /opt lives in the cell's root filesystem, which a container-driver pause discards,
|
|
17176
|
+
* while an image with the runtime baked in skips the install entirely.
|
|
17177
|
+
*/
|
|
17178
|
+
/**
|
|
17179
|
+
* Clone a GitHub repository into /workspace on a new working branch, through Hotcell's
|
|
17180
|
+
* credential gateway: the remote points at the gateway and git authenticates with the
|
|
17181
|
+
* cell's own egress token, so the real GitHub token never enters the cell. Idempotent.
|
|
17182
|
+
*/
|
|
17183
|
+
async cloneRepository(cell, project) {
|
|
17184
|
+
const seeded = await cell.exec(`test -f ${CELL_STATE_DIR}/.seeded`);
|
|
17185
|
+
if (seeded.exitCode === 0) return;
|
|
17186
|
+
const { repository, baseBranch, branch } = project;
|
|
17187
|
+
if (!repository || !REPOSITORY_RE.test(repository) || !baseBranch || !BRANCH_RE.test(baseBranch) || !branch || !BRANCH_RE.test(branch)) {
|
|
17188
|
+
throw new Error("invalid_project");
|
|
17189
|
+
}
|
|
17190
|
+
await cell.mkdir(CELL_STATE_DIR, { parents: true });
|
|
17191
|
+
const clone = await cell.exec(
|
|
17192
|
+
`set -e; cd ${CELL_WORKSPACE}; if [ -n "$ASTER_GITHUB_GIT_URL" ]; then remote="$ASTER_GITHUB_GIT_URL/${repository}.git"; else remote="\${GITHUB_BASE_URL%/github}/github-git/${repository}.git"; fi; git init -q; git remote add origin "$remote" 2>/dev/null || git remote set-url origin "$remote"; git config credential.helper '!f() { echo username=x-access-token; echo "password=\${ASTER_GITHUB_TOKEN:-$GITHUB_API_KEY}"; }; f'; git config user.name "Aster Autopilot"; git config user.email "autopilot@aster.invalid"; if git ls-remote --exit-code --heads origin '${baseBranch}' >/dev/null 2>&1; then git fetch -q origin '${baseBranch}'; git checkout -q -b '${branch}' FETCH_HEAD; else git checkout -q -b '${branch}'; fi; touch ${CELL_STATE_DIR}/.seeded`
|
|
17193
|
+
);
|
|
17194
|
+
if (clone.exitCode !== 0) throw new Error("repository_clone_failed");
|
|
17195
|
+
}
|
|
17196
|
+
/** Files under /workspace/artifacts, the folder the agent saves user-facing deliverables in. */
|
|
17197
|
+
async listArtifacts(cellId) {
|
|
17198
|
+
const cell = await this.provider.getCell(cellId);
|
|
17199
|
+
const result2 = await cell.exec(
|
|
17200
|
+
`[ -d ${ARTIFACTS_DIR} ] || exit 0; cd ${ARTIFACTS_DIR} && find . -type f ! -path '*/.*' -printf '%P\\t%s\\t%T@\\n' | head -n ${MAX_ARTIFACTS}`
|
|
17201
|
+
);
|
|
17202
|
+
if (result2.exitCode !== 0) return [];
|
|
17203
|
+
return result2.stdout.split("\n").filter(Boolean).flatMap((line) => {
|
|
17204
|
+
const [path = "", size = "", modified = ""] = line.split(" ");
|
|
17205
|
+
if (!isSafeArtifactPath(path)) return [];
|
|
17206
|
+
return [{
|
|
17207
|
+
path,
|
|
17208
|
+
name: path.split("/").pop() ?? path,
|
|
17209
|
+
size: Number(size) || 0,
|
|
17210
|
+
modifiedAt: new Date(Math.round(Number(modified) * 1e3) || Date.now()).toISOString(),
|
|
17211
|
+
mimeType: artifactMimeType(path)
|
|
17212
|
+
}];
|
|
17213
|
+
}).sort((a, b) => b.modifiedAt.localeCompare(a.modifiedAt));
|
|
17214
|
+
}
|
|
17215
|
+
/** The bytes of one artifact, at most 50 MB. */
|
|
17216
|
+
async readArtifact(cellId, path) {
|
|
17217
|
+
if (!isSafeArtifactPath(path)) throw new Error("invalid_artifact_path");
|
|
17218
|
+
const cell = await this.provider.getCell(cellId);
|
|
17219
|
+
const file = shellQuote2(`${ARTIFACTS_DIR}/${path}`);
|
|
17220
|
+
const stat10 = await cell.exec(`[ -f ${file} ] && [ ! -L ${file} ] && stat -c %s -- ${file}`);
|
|
17221
|
+
if (stat10.exitCode !== 0) throw new Error("artifact_not_found");
|
|
17222
|
+
if (Number(stat10.stdout.trim()) > MAX_ARTIFACT_BYTES) throw new Error("artifact_too_large");
|
|
17223
|
+
const encoded = await cell.exec(`base64 -w0 -- ${file}`);
|
|
17224
|
+
if (encoded.exitCode !== 0) throw new Error("artifact_not_found");
|
|
17225
|
+
return { data: Buffer.from(encoded.stdout.trim(), "base64"), mimeType: artifactMimeType(path), name: path.split("/").pop() ?? path };
|
|
17226
|
+
}
|
|
17227
|
+
/** Notes are prepended to the agent's next prompt once; kept in the cell so they survive restarts. */
|
|
17228
|
+
async addNote(cell, text) {
|
|
17229
|
+
const existing = await this.readNotes(cell);
|
|
17230
|
+
await cell.mkdir(CELL_STATE_DIR, { parents: true });
|
|
17231
|
+
await cell.writeFile(NOTES_FILE, existing ? `${existing}
|
|
17232
|
+
|
|
17233
|
+
${text}` : text, { mode: "0600" });
|
|
17234
|
+
}
|
|
17235
|
+
async readNotes(cell) {
|
|
17236
|
+
try {
|
|
17237
|
+
return (await cell.readFile(NOTES_FILE)).trim();
|
|
17238
|
+
} catch {
|
|
17239
|
+
return "";
|
|
17240
|
+
}
|
|
17241
|
+
}
|
|
17242
|
+
async clearNotes(cell) {
|
|
17243
|
+
await cell.exec(`rm -f ${NOTES_FILE}`);
|
|
17244
|
+
}
|
|
15779
17245
|
async installRuntime(cell) {
|
|
15780
|
-
const
|
|
17246
|
+
const expectedVersion = /@(\d[^@/]*)$/.exec(this.packageSpec)?.[1];
|
|
15781
17247
|
try {
|
|
15782
|
-
|
|
17248
|
+
const installed = JSON.parse(await cell.readFile(INSTALLED_RUNTIME_PACKAGE));
|
|
17249
|
+
if (!expectedVersion || installed.version === expectedVersion) return;
|
|
15783
17250
|
} catch {
|
|
15784
17251
|
}
|
|
15785
17252
|
const install = await cell.exec(`mkdir -p /opt/autopilot && npm install --prefix /opt/autopilot --no-audit --no-fund ${this.packageSpec}`);
|
|
15786
17253
|
if (install.exitCode !== 0) throw new Error("cell_runtime_install_failed");
|
|
15787
|
-
await cell.writeFile(marker, this.packageSpec);
|
|
15788
17254
|
}
|
|
15789
17255
|
async ensureBridge(cell) {
|
|
15790
17256
|
const processes = await cell.listProcesses().catch(() => []);
|
|
@@ -15797,7 +17263,7 @@ var init_aster_cell_runtime = __esm({
|
|
|
15797
17263
|
for (; ; ) {
|
|
15798
17264
|
try {
|
|
15799
17265
|
const health = await this.control(cell, "GET", "/health");
|
|
15800
|
-
if (health.ok === true) return;
|
|
17266
|
+
if (health.ok === true && (!health.sessionId || health.initialized === true)) return;
|
|
15801
17267
|
} catch {
|
|
15802
17268
|
}
|
|
15803
17269
|
if (Date.now() > deadline) throw new Error("cell_bridge_unhealthy");
|
|
@@ -15805,7 +17271,7 @@ var init_aster_cell_runtime = __esm({
|
|
|
15805
17271
|
}
|
|
15806
17272
|
}
|
|
15807
17273
|
async control(cell, method, path, body) {
|
|
15808
|
-
const requestId =
|
|
17274
|
+
const requestId = randomUUID8();
|
|
15809
17275
|
await cell.mkdir(CELL_CONTROL_DIR, { parents: true });
|
|
15810
17276
|
await cell.writeFile(`${CELL_CONTROL_DIR}/${requestId}.json`, JSON.stringify({ method, path, body }), { mode: "0600" });
|
|
15811
17277
|
const result2 = await cell.exec(`${CONTROL_RUNNER} ${requestId}`);
|
|
@@ -15819,6 +17285,31 @@ var init_aster_cell_runtime = __esm({
|
|
|
15819
17285
|
return this.control(cell, method, path, body);
|
|
15820
17286
|
}
|
|
15821
17287
|
};
|
|
17288
|
+
ARTIFACT_MIME_TYPES = {
|
|
17289
|
+
pdf: "application/pdf",
|
|
17290
|
+
html: "text/html",
|
|
17291
|
+
htm: "text/html",
|
|
17292
|
+
md: "text/markdown",
|
|
17293
|
+
txt: "text/plain",
|
|
17294
|
+
csv: "text/csv",
|
|
17295
|
+
json: "application/json",
|
|
17296
|
+
png: "image/png",
|
|
17297
|
+
jpg: "image/jpeg",
|
|
17298
|
+
jpeg: "image/jpeg",
|
|
17299
|
+
gif: "image/gif",
|
|
17300
|
+
webp: "image/webp",
|
|
17301
|
+
svg: "image/svg+xml",
|
|
17302
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
17303
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
17304
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
17305
|
+
key: "application/vnd.apple.keynote",
|
|
17306
|
+
pages: "application/vnd.apple.pages",
|
|
17307
|
+
numbers: "application/vnd.apple.numbers",
|
|
17308
|
+
zip: "application/zip",
|
|
17309
|
+
mp4: "video/mp4",
|
|
17310
|
+
mov: "video/quicktime",
|
|
17311
|
+
mp3: "audio/mpeg"
|
|
17312
|
+
};
|
|
15822
17313
|
}
|
|
15823
17314
|
});
|
|
15824
17315
|
|
|
@@ -15831,8 +17322,8 @@ function validateConfig(config2) {
|
|
|
15831
17322
|
throw new HotcellProviderError("Hotcell must use HTTPS unless the control endpoint is loopback", "insecure_hotcell_endpoint");
|
|
15832
17323
|
}
|
|
15833
17324
|
if (!config2.apiKey.trim()) throw new HotcellProviderError("Hotcell API key is required", "hotcell_auth_required");
|
|
15834
|
-
if (
|
|
15835
|
-
throw new HotcellProviderError("Aster requires an explicit Hotcell
|
|
17325
|
+
if (!HOTCELL_DRIVERS.includes(config2.driver)) {
|
|
17326
|
+
throw new HotcellProviderError("Aster requires an explicit Hotcell driver", "unsafe_hotcell_driver");
|
|
15836
17327
|
}
|
|
15837
17328
|
if (!Number.isInteger(config2.memoryMb) || config2.memoryMb < 512 || !Number.isFinite(config2.cpus) || config2.cpus < 0.5 || !Number.isInteger(config2.pidsLimit) || config2.pidsLimit < 32) {
|
|
15838
17329
|
throw new HotcellProviderError("Hotcell resource limits are invalid", "invalid_hotcell_limits");
|
|
@@ -15856,11 +17347,12 @@ function numberEnv(value, fallback, min, max) {
|
|
|
15856
17347
|
if (!Number.isFinite(parsed) || parsed < min || parsed > max) throw new HotcellProviderError("Invalid Hotcell resource configuration", "invalid_hotcell_config");
|
|
15857
17348
|
return parsed;
|
|
15858
17349
|
}
|
|
15859
|
-
var HOTCELL_CONVERSATION_LABEL, HotcellProviderError, HotcellProvider;
|
|
17350
|
+
var HOTCELL_CONVERSATION_LABEL, HOTCELL_DRIVERS, HotcellProviderError, HotcellProvider;
|
|
15860
17351
|
var init_hotcell_provider = __esm({
|
|
15861
17352
|
"src/server/hotcell-provider.ts"() {
|
|
15862
17353
|
"use strict";
|
|
15863
17354
|
HOTCELL_CONVERSATION_LABEL = "autopilot.conversation_id";
|
|
17355
|
+
HOTCELL_DRIVERS = ["applevz", "firecracker", "container"];
|
|
15864
17356
|
HotcellProviderError = class extends Error {
|
|
15865
17357
|
constructor(message2, code) {
|
|
15866
17358
|
super(message2);
|
|
@@ -15881,9 +17373,9 @@ var init_hotcell_provider = __esm({
|
|
|
15881
17373
|
const endpoint = env2.AUTOPILOT_HOTCELL_ENDPOINT;
|
|
15882
17374
|
const apiKey = env2.AUTOPILOT_HOTCELL_API_KEY;
|
|
15883
17375
|
const driver = env2.AUTOPILOT_HOTCELL_DRIVER;
|
|
15884
|
-
if (!endpoint || !apiKey || driver
|
|
17376
|
+
if (!endpoint || !apiKey || !HOTCELL_DRIVERS.includes(driver)) {
|
|
15885
17377
|
throw new HotcellProviderError(
|
|
15886
|
-
"Aster Hotcell requires AUTOPILOT_HOTCELL_ENDPOINT, AUTOPILOT_HOTCELL_API_KEY, and an explicit AUTOPILOT_HOTCELL_DRIVER (applevz or
|
|
17378
|
+
"Aster Hotcell requires AUTOPILOT_HOTCELL_ENDPOINT, AUTOPILOT_HOTCELL_API_KEY, and an explicit AUTOPILOT_HOTCELL_DRIVER (applevz, firecracker, or container)",
|
|
15887
17379
|
"hotcell_not_configured"
|
|
15888
17380
|
);
|
|
15889
17381
|
}
|
|
@@ -15891,6 +17383,11 @@ var init_hotcell_provider = __esm({
|
|
|
15891
17383
|
endpoint,
|
|
15892
17384
|
apiKey,
|
|
15893
17385
|
driver,
|
|
17386
|
+
networked: env2.AUTOPILOT_HOTCELL_NETWORKED === "true",
|
|
17387
|
+
anyModel: env2.AUTOPILOT_HOTCELL_ANY_MODEL === "true",
|
|
17388
|
+
sleepAfterMs: integerEnv(env2.AUTOPILOT_HOTCELL_SLEEP_AFTER_MS, 0, 0, 7 * 24 * 60 * 60 * 1e3),
|
|
17389
|
+
...env2.AUTOPILOT_HOTCELL_IMAGE ? { image: env2.AUTOPILOT_HOTCELL_IMAGE } : {},
|
|
17390
|
+
...env2.AUTOPILOT_HOTCELL_CHAT_MEMORY_MB ? { chatMemoryMb: integerEnv(env2.AUTOPILOT_HOTCELL_CHAT_MEMORY_MB, 1024, 512, 65536) } : {},
|
|
15894
17391
|
memoryMb: integerEnv(env2.AUTOPILOT_HOTCELL_MEMORY_MB, 4096, 512, 65536),
|
|
15895
17392
|
cpus: numberEnv(env2.AUTOPILOT_HOTCELL_CPUS, 2, 0.5, 64),
|
|
15896
17393
|
pidsLimit: integerEnv(env2.AUTOPILOT_HOTCELL_PIDS, 256, 32, 8192),
|
|
@@ -15915,23 +17412,28 @@ var init_hotcell_provider = __esm({
|
|
|
15915
17412
|
const info = await this.client.info();
|
|
15916
17413
|
if (!info.auth) throw new HotcellProviderError("Hotcell API authentication is disabled; refusing to provision an Aster cell", "hotcell_auth_required");
|
|
15917
17414
|
if (!info.drivers.includes(this.config.driver)) {
|
|
15918
|
-
throw new HotcellProviderError("Configured Hotcell
|
|
17415
|
+
throw new HotcellProviderError("Configured Hotcell driver is unavailable", "hotcell_driver_unavailable");
|
|
15919
17416
|
}
|
|
15920
17417
|
if (!info.egressProviders.includes("openrouter")) {
|
|
15921
17418
|
throw new HotcellProviderError("Hotcell OpenRouter credential gateway is unavailable", "hotcell_egress_unavailable");
|
|
15922
17419
|
}
|
|
17420
|
+
if (input.kind === "code" && !input.githubViaProxy && !info.egressProviders.includes("github")) {
|
|
17421
|
+
throw new HotcellProviderError("Hotcell GitHub credential gateway is unavailable", "hotcell_github_unavailable");
|
|
17422
|
+
}
|
|
15923
17423
|
const options = {
|
|
15924
17424
|
driver: this.config.driver,
|
|
15925
|
-
|
|
17425
|
+
...this.config.image ? { image: this.config.image } : {},
|
|
17426
|
+
...input.env ? { env: input.env } : {},
|
|
17427
|
+
networked: this.config.networked === true,
|
|
15926
17428
|
persist: true,
|
|
15927
|
-
sleepAfter: 0,
|
|
15928
|
-
memoryMb: this.config.memoryMb,
|
|
17429
|
+
sleepAfter: this.config.sleepAfterMs ?? 0,
|
|
17430
|
+
memoryMb: input.kind === "chat" ? this.config.chatMemoryMb ?? this.config.memoryMb : this.config.memoryMb,
|
|
15929
17431
|
cpus: this.config.cpus,
|
|
15930
17432
|
pidsLimit: this.config.pidsLimit,
|
|
15931
17433
|
egressSpendCapUsd: this.config.spendCapUsd,
|
|
15932
17434
|
egress: {
|
|
15933
|
-
providers: ["openrouter"],
|
|
15934
|
-
models: [input.model],
|
|
17435
|
+
providers: input.kind === "code" && !input.githubViaProxy ? ["openrouter", "github", "github-git"] : ["openrouter"],
|
|
17436
|
+
...this.config.anyModel ? {} : { models: [input.model] },
|
|
15935
17437
|
spendCapUsd: this.config.spendCapUsd,
|
|
15936
17438
|
ttlMs: this.config.tokenTtlMs
|
|
15937
17439
|
},
|
|
@@ -15961,6 +17463,15 @@ var init_hotcell_provider = __esm({
|
|
|
15961
17463
|
for (const token of tokens) await cell.revokeEgressToken(token.token);
|
|
15962
17464
|
await cell.destroy();
|
|
15963
17465
|
}
|
|
17466
|
+
/** Aster conversation cells with their lifecycle status and last API activity. */
|
|
17467
|
+
async listConversationCells() {
|
|
17468
|
+
return (await this.client.list()).filter((item) => item.labels?.[HOTCELL_CONVERSATION_LABEL]).map((item) => ({
|
|
17469
|
+
id: item.id,
|
|
17470
|
+
conversationId: item.labels[HOTCELL_CONVERSATION_LABEL],
|
|
17471
|
+
status: item.status,
|
|
17472
|
+
lastActivityAt: item.lastActivityAt
|
|
17473
|
+
}));
|
|
17474
|
+
}
|
|
15964
17475
|
/** True suspend: on microVM drivers the daemon snapshots memory, so the Autopilot session resumes alive. */
|
|
15965
17476
|
async pauseConversationCell(cellId) {
|
|
15966
17477
|
if (!cellId.trim()) throw new HotcellProviderError("Missing Hotcell id", "hotcell_id_missing");
|
|
@@ -15975,13 +17486,16 @@ var init_hotcell_provider = __esm({
|
|
|
15975
17486
|
});
|
|
15976
17487
|
|
|
15977
17488
|
// src/server/routes.ts
|
|
15978
|
-
import { randomUUID as
|
|
17489
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
15979
17490
|
import { URL as URL2 } from "url";
|
|
15980
|
-
import { readFile as
|
|
17491
|
+
import { readFile as readFile21, unlink as unlink3 } from "fs/promises";
|
|
15981
17492
|
import { resolve as resolve9, basename as basename5 } from "path";
|
|
15982
17493
|
function createHotcellAsterRuntime() {
|
|
15983
17494
|
try {
|
|
15984
|
-
|
|
17495
|
+
const idlePauseMs = Number(process.env.AUTOPILOT_ASTER_IDLE_PAUSE_MS ?? 15 * 60 * 1e3);
|
|
17496
|
+
return new AsterHotcellRuntime(HotcellProvider.fromEnvironment(), {
|
|
17497
|
+
idlePauseMs: Number.isFinite(idlePauseMs) && idlePauseMs >= 0 ? idlePauseMs : 15 * 60 * 1e3
|
|
17498
|
+
});
|
|
15985
17499
|
} catch (error) {
|
|
15986
17500
|
if (error instanceof HotcellProviderError && error.code === "hotcell_not_configured") return void 0;
|
|
15987
17501
|
throw error;
|
|
@@ -16067,7 +17581,7 @@ async function buildUserMessage2(prompt, files, cwd) {
|
|
|
16067
17581
|
}
|
|
16068
17582
|
} else {
|
|
16069
17583
|
try {
|
|
16070
|
-
const content = await
|
|
17584
|
+
const content = await readFile21(filePath, "utf8");
|
|
16071
17585
|
const relPath = filePath.startsWith(cwd) ? filePath.slice(cwd.length + 1) : filePath;
|
|
16072
17586
|
fileContents.push(`
|
|
16073
17587
|
--- ${relPath} ---
|
|
@@ -16146,6 +17660,12 @@ function setupRoutes(config2, options = {}) {
|
|
|
16146
17660
|
cancelRun: () => {
|
|
16147
17661
|
}
|
|
16148
17662
|
});
|
|
17663
|
+
if (!options.asterRuntime) {
|
|
17664
|
+
void asterApi.startGitHubProxy().catch((error) => {
|
|
17665
|
+
process.stderr.write(`aster: GitHub proxy failed to start: ${error instanceof Error ? error.message : String(error)}
|
|
17666
|
+
`);
|
|
17667
|
+
});
|
|
17668
|
+
}
|
|
16149
17669
|
const wakeScheduler = new RunWakeScheduler({
|
|
16150
17670
|
onWake: async (event) => {
|
|
16151
17671
|
const store = new RunStore();
|
|
@@ -16288,7 +17808,7 @@ function setupRoutes(config2, options = {}) {
|
|
|
16288
17808
|
const executor = new ToolExecutor(tools);
|
|
16289
17809
|
const { makeSessionId: makeSessionId2 } = await Promise.resolve().then(() => (init_sessions(), sessions_exports));
|
|
16290
17810
|
const sessionId = makeSessionId2(task);
|
|
16291
|
-
const runId =
|
|
17811
|
+
const runId = randomUUID9();
|
|
16292
17812
|
const worktreeManager = new RunWorktreeManager();
|
|
16293
17813
|
let worktree;
|
|
16294
17814
|
if (useWorktree) {
|
|
@@ -16870,10 +18390,10 @@ var server_exports = {};
|
|
|
16870
18390
|
__export(server_exports, {
|
|
16871
18391
|
startServer: () => startServer
|
|
16872
18392
|
});
|
|
16873
|
-
import { createServer } from "http";
|
|
18393
|
+
import { createServer as createServer2 } from "http";
|
|
16874
18394
|
async function startServer(opts2) {
|
|
16875
18395
|
const { handleRequest, cleanup } = setupRoutes(opts2.config, { asterRuntime: opts2.asterRuntime });
|
|
16876
|
-
const server =
|
|
18396
|
+
const server = createServer2((req, res) => {
|
|
16877
18397
|
const isAsterApi = req.url?.startsWith("/api/v1/") === true;
|
|
16878
18398
|
if (!isAsterApi) {
|
|
16879
18399
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
@@ -16972,9 +18492,9 @@ __export(db_exports, {
|
|
|
16972
18492
|
updateAccessedAt: () => updateAccessedAt,
|
|
16973
18493
|
updateMemoryEmbedding: () => updateMemoryEmbedding
|
|
16974
18494
|
});
|
|
16975
|
-
import
|
|
16976
|
-
import { dirname as
|
|
16977
|
-
import { mkdirSync as
|
|
18495
|
+
import Database6 from "better-sqlite3";
|
|
18496
|
+
import { dirname as dirname17 } from "path";
|
|
18497
|
+
import { mkdirSync as mkdirSync8, statSync as statSync4 } from "fs";
|
|
16978
18498
|
function initSchema(db) {
|
|
16979
18499
|
db.exec(`
|
|
16980
18500
|
CREATE TABLE IF NOT EXISTS memories (
|
|
@@ -17058,8 +18578,8 @@ function openMemoryDb(dbPath) {
|
|
|
17058
18578
|
if (dbInstance) {
|
|
17059
18579
|
dbInstance.close();
|
|
17060
18580
|
}
|
|
17061
|
-
|
|
17062
|
-
dbInstance = new
|
|
18581
|
+
mkdirSync8(dirname17(dbPath), { recursive: true });
|
|
18582
|
+
dbInstance = new Database6(dbPath);
|
|
17063
18583
|
dbInstance.pragma("journal_mode = WAL");
|
|
17064
18584
|
dbInstance.pragma("foreign_keys = ON");
|
|
17065
18585
|
initSchema(dbInstance);
|
|
@@ -19013,120 +20533,6 @@ var init_lsp = __esm({
|
|
|
19013
20533
|
}
|
|
19014
20534
|
});
|
|
19015
20535
|
|
|
19016
|
-
// src/models/openrouter-catalog.ts
|
|
19017
|
-
var openrouter_catalog_exports = {};
|
|
19018
|
-
__export(openrouter_catalog_exports, {
|
|
19019
|
-
DEFAULT_CACHE_TTL_MS: () => DEFAULT_CACHE_TTL_MS,
|
|
19020
|
-
ensureOpenRouterCatalog: () => ensureOpenRouterCatalog,
|
|
19021
|
-
fetchOpenRouterCatalog: () => fetchOpenRouterCatalog,
|
|
19022
|
-
loadOpenRouterCatalog: () => loadOpenRouterCatalog,
|
|
19023
|
-
mapOpenRouterModel: () => mapOpenRouterModel
|
|
19024
|
-
});
|
|
19025
|
-
import { readFile as readFile21, writeFile as writeFile12, mkdir as mkdir13 } from "fs/promises";
|
|
19026
|
-
import { homedir as homedir22 } from "os";
|
|
19027
|
-
import { join as join31, dirname as dirname16 } from "path";
|
|
19028
|
-
function catalogCachePath() {
|
|
19029
|
-
const xdg = process.env.XDG_CONFIG_HOME || join31(homedir22(), ".config");
|
|
19030
|
-
return join31(xdg, "kimiflare", "openrouter-models.json");
|
|
19031
|
-
}
|
|
19032
|
-
function perMtok(perToken) {
|
|
19033
|
-
const n = Number(perToken);
|
|
19034
|
-
return Number.isFinite(n) ? n * 1e6 : 0;
|
|
19035
|
-
}
|
|
19036
|
-
function mapOpenRouterModel(raw) {
|
|
19037
|
-
const supported = new Set(raw.supported_parameters ?? []);
|
|
19038
|
-
const aa = raw.benchmarks?.artificial_analysis;
|
|
19039
|
-
const quality = {
|
|
19040
|
-
...typeof aa?.coding_index === "number" ? { coding: aa.coding_index } : {},
|
|
19041
|
-
...typeof aa?.agentic_index === "number" ? { agentic: aa.agentic_index } : {},
|
|
19042
|
-
...typeof aa?.intelligence_index === "number" ? { intelligence: aa.intelligence_index } : {}
|
|
19043
|
-
};
|
|
19044
|
-
return {
|
|
19045
|
-
id: raw.id,
|
|
19046
|
-
...raw.name ? { name: raw.name } : {},
|
|
19047
|
-
...typeof raw.created === "number" ? { created: raw.created } : {},
|
|
19048
|
-
...Object.keys(quality).length > 0 ? { quality } : {},
|
|
19049
|
-
...raw.supported_parameters ? { parameters: [...raw.supported_parameters] } : {},
|
|
19050
|
-
contextWindow: raw.context_length ?? 128e3,
|
|
19051
|
-
maxOutputTokens: raw.top_provider?.max_completion_tokens ?? 4096,
|
|
19052
|
-
pricing: {
|
|
19053
|
-
inputPerMtok: perMtok(raw.pricing?.prompt),
|
|
19054
|
-
...raw.pricing?.input_cache_read ? { cachedInputPerMtok: perMtok(raw.pricing.input_cache_read) } : {},
|
|
19055
|
-
outputPerMtok: perMtok(raw.pricing?.completion)
|
|
19056
|
-
},
|
|
19057
|
-
supports: {
|
|
19058
|
-
// OpenRouter's own catalog is the authority on whether a model accepts tool
|
|
19059
|
-
// calls; absence of "tools" here means the upstream provider doesn't support
|
|
19060
|
-
// function calling for this model, and we must not claim otherwise.
|
|
19061
|
-
tools: supported.has("tools"),
|
|
19062
|
-
reasoning: supported.has("reasoning") || supported.has("include_reasoning"),
|
|
19063
|
-
streaming: true,
|
|
19064
|
-
vision: (raw.architecture?.input_modalities ?? []).includes("image"),
|
|
19065
|
-
temperature: supported.has("temperature") ? true : void 0
|
|
19066
|
-
}
|
|
19067
|
-
};
|
|
19068
|
-
}
|
|
19069
|
-
async function fetchOpenRouterCatalog(fetchImpl2 = fetch) {
|
|
19070
|
-
const res = await fetchWithNetworkRetry(fetchImpl2, openRouterUrl("models"), {
|
|
19071
|
-
headers: { Accept: "application/json" }
|
|
19072
|
-
});
|
|
19073
|
-
if (!res.ok) {
|
|
19074
|
-
throw new Error(`OpenRouter /models returned HTTP ${res.status}`);
|
|
19075
|
-
}
|
|
19076
|
-
const body = await res.json();
|
|
19077
|
-
return (body.data ?? []).map(mapOpenRouterModel);
|
|
19078
|
-
}
|
|
19079
|
-
async function readCache3() {
|
|
19080
|
-
try {
|
|
19081
|
-
const raw = await readFile21(catalogCachePath(), "utf8");
|
|
19082
|
-
const parsed = JSON.parse(raw);
|
|
19083
|
-
return parsed.version === CACHE_VERSION ? parsed : null;
|
|
19084
|
-
} catch {
|
|
19085
|
-
return null;
|
|
19086
|
-
}
|
|
19087
|
-
}
|
|
19088
|
-
async function writeCache3(models) {
|
|
19089
|
-
const path = catalogCachePath();
|
|
19090
|
-
try {
|
|
19091
|
-
await mkdir13(dirname16(path), { recursive: true });
|
|
19092
|
-
const cache = { version: CACHE_VERSION, fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), models };
|
|
19093
|
-
await writeFile12(path, JSON.stringify(cache, null, 2), "utf8");
|
|
19094
|
-
} catch {
|
|
19095
|
-
}
|
|
19096
|
-
}
|
|
19097
|
-
async function loadOpenRouterCatalog(opts2 = {}) {
|
|
19098
|
-
const ttlMs = opts2.ttlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
19099
|
-
const cached = await readCache3();
|
|
19100
|
-
const isFresh = cached && Date.now() - Date.parse(cached.fetchedAt) < ttlMs;
|
|
19101
|
-
if (isFresh) return cached.models;
|
|
19102
|
-
try {
|
|
19103
|
-
const models = await fetchOpenRouterCatalog(opts2.fetchImpl ?? fetch);
|
|
19104
|
-
await writeCache3(models);
|
|
19105
|
-
return models;
|
|
19106
|
-
} catch {
|
|
19107
|
-
return cached?.models ?? [];
|
|
19108
|
-
}
|
|
19109
|
-
}
|
|
19110
|
-
async function ensureOpenRouterCatalog(opts2 = {}) {
|
|
19111
|
-
try {
|
|
19112
|
-
const models = await loadOpenRouterCatalog(opts2);
|
|
19113
|
-
if (models.length > 0) registerOpenRouterModels(models);
|
|
19114
|
-
return models.length;
|
|
19115
|
-
} catch {
|
|
19116
|
-
return 0;
|
|
19117
|
-
}
|
|
19118
|
-
}
|
|
19119
|
-
var CACHE_VERSION, DEFAULT_CACHE_TTL_MS;
|
|
19120
|
-
var init_openrouter_catalog = __esm({
|
|
19121
|
-
"src/models/openrouter-catalog.ts"() {
|
|
19122
|
-
"use strict";
|
|
19123
|
-
init_registry();
|
|
19124
|
-
init_openrouter();
|
|
19125
|
-
CACHE_VERSION = 4;
|
|
19126
|
-
DEFAULT_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
19127
|
-
}
|
|
19128
|
-
});
|
|
19129
|
-
|
|
19130
20536
|
// src/models/requesty-catalog.ts
|
|
19131
20537
|
var requesty_catalog_exports = {};
|
|
19132
20538
|
__export(requesty_catalog_exports, {
|
|
@@ -19139,7 +20545,7 @@ __export(requesty_catalog_exports, {
|
|
|
19139
20545
|
});
|
|
19140
20546
|
import { readFile as readFile22, writeFile as writeFile13, mkdir as mkdir14 } from "fs/promises";
|
|
19141
20547
|
import { homedir as homedir23 } from "os";
|
|
19142
|
-
import { join as join32, dirname as
|
|
20548
|
+
import { join as join32, dirname as dirname18 } from "path";
|
|
19143
20549
|
function catalogCachePath2() {
|
|
19144
20550
|
const xdg = process.env.XDG_CONFIG_HOME || join32(homedir23(), ".config");
|
|
19145
20551
|
return join32(xdg, "kimiflare", "requesty-models.json");
|
|
@@ -19213,7 +20619,7 @@ async function readCache4() {
|
|
|
19213
20619
|
async function writeCache4(catalog) {
|
|
19214
20620
|
const path = catalogCachePath2();
|
|
19215
20621
|
try {
|
|
19216
|
-
await mkdir14(
|
|
20622
|
+
await mkdir14(dirname18(path), { recursive: true });
|
|
19217
20623
|
const cache = { version: CACHE_VERSION2, fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), ...catalog };
|
|
19218
20624
|
await writeFile13(path, JSON.stringify(cache, null, 2), "utf8");
|
|
19219
20625
|
} catch {
|
|
@@ -21261,7 +22667,7 @@ __export(app_helpers_exports, {
|
|
|
21261
22667
|
trackRecentFile: () => trackRecentFile
|
|
21262
22668
|
});
|
|
21263
22669
|
import { execSync as execSync3, spawn as spawn6 } from "child_process";
|
|
21264
|
-
import { existsSync as existsSync5, readFileSync as
|
|
22670
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6, statSync as statSync5 } from "fs";
|
|
21265
22671
|
import { join as join34 } from "path";
|
|
21266
22672
|
import { platform as platform3 } from "os";
|
|
21267
22673
|
function replaceQueuedPrompt(queue2, key, full, display) {
|
|
@@ -21375,7 +22781,7 @@ function buildFilePickerIgnoreList(cwd) {
|
|
|
21375
22781
|
if (stats.size > MAX_GITIGNORE_SIZE) {
|
|
21376
22782
|
return hardcoded;
|
|
21377
22783
|
}
|
|
21378
|
-
const content =
|
|
22784
|
+
const content = readFileSync6(gitignorePath, "utf-8");
|
|
21379
22785
|
for (const line of content.split(/\r?\n/)) {
|
|
21380
22786
|
const trimmed = line.trim();
|
|
21381
22787
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -25833,6 +27239,8 @@ function humanizeToolTitle(toolName, originalTitle, tier) {
|
|
|
25833
27239
|
return humanizeMcpTitle(toolName, originalTitle, tier);
|
|
25834
27240
|
}
|
|
25835
27241
|
switch (toolName) {
|
|
27242
|
+
case "execute_code":
|
|
27243
|
+
return "Running code batch";
|
|
25836
27244
|
case "read":
|
|
25837
27245
|
return pick(tier, {
|
|
25838
27246
|
light: `Taking a quick look at ${extractPath(originalTitle)}`,
|
|
@@ -26523,6 +27931,7 @@ function isSettled(e) {
|
|
|
26523
27931
|
if (e.kind === "assistant") return !e.streaming;
|
|
26524
27932
|
if (e.kind === "tool") return e.status !== "running" && e.status !== "queued";
|
|
26525
27933
|
if (e.kind === "user") return !e.queued;
|
|
27934
|
+
if (e.kind === "info") return !e.pending;
|
|
26526
27935
|
return true;
|
|
26527
27936
|
}
|
|
26528
27937
|
function firstUnsettledIndex(events) {
|
|
@@ -29447,12 +30856,12 @@ auto (default) lets the coordinator assess substantial tasks and delegate indepe
|
|
|
29447
30856
|
setEvents((events) => [
|
|
29448
30857
|
...events,
|
|
29449
30858
|
{ kind: "user", key: mkKey2(), text: `/jev ${rest.join(" ")}` },
|
|
29450
|
-
{ kind: "info", key: eventKey, text: "Jev is evaluating\u2026" }
|
|
30859
|
+
{ kind: "info", key: eventKey, text: "Jev is evaluating\u2026", pending: true }
|
|
29451
30860
|
]);
|
|
29452
30861
|
try {
|
|
29453
30862
|
const context = noContext ? [] : await buildJevContext(question.prompt, ctx.messagesRef.current, process.cwd());
|
|
29454
30863
|
const receipt = formatJevContextReceipt(context, showContext);
|
|
29455
|
-
setResult(eventKey, { kind: "info", key: eventKey, text: `Jev is evaluating \xB7 ${receipt}
|
|
30864
|
+
setResult(eventKey, { kind: "info", key: eventKey, text: `Jev is evaluating \xB7 ${receipt}`, pending: true });
|
|
29456
30865
|
await new Promise((resolve15) => setTimeout(resolve15, 0));
|
|
29457
30866
|
const answer = await askJev(apiKey, question, { context });
|
|
29458
30867
|
const presentation = presentJevAnswer(question, answer);
|
|
@@ -30905,7 +32314,7 @@ __export(openrouter_oauth_exports, {
|
|
|
30905
32314
|
startLoopbackListener: () => startLoopbackListener
|
|
30906
32315
|
});
|
|
30907
32316
|
import { createHash as createHash4, randomBytes as randomBytes3 } from "crypto";
|
|
30908
|
-
import { createServer as
|
|
32317
|
+
import { createServer as createServer3 } from "http";
|
|
30909
32318
|
import { hostname } from "os";
|
|
30910
32319
|
function createPkce() {
|
|
30911
32320
|
const verifier = randomBytes3(32).toString("base64url");
|
|
@@ -30961,7 +32370,7 @@ async function startLoopbackListener(opts2 = {}) {
|
|
|
30961
32370
|
});
|
|
30962
32371
|
code.catch(() => void 0);
|
|
30963
32372
|
let settled = false;
|
|
30964
|
-
const server =
|
|
32373
|
+
const server = createServer3((req, res) => {
|
|
30965
32374
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
30966
32375
|
if (url.pathname !== "/callback") {
|
|
30967
32376
|
res.writeHead(404).end();
|
|
@@ -36285,7 +37694,7 @@ var init_run_compact = __esm({
|
|
|
36285
37694
|
|
|
36286
37695
|
// src/commands/save.ts
|
|
36287
37696
|
import { mkdir as mkdir16, writeFile as writeFile15, unlink as unlink5 } from "fs/promises";
|
|
36288
|
-
import { dirname as
|
|
37697
|
+
import { dirname as dirname20 } from "path";
|
|
36289
37698
|
async function saveCustomCommand(opts2) {
|
|
36290
37699
|
const dir = opts2.source === "project" ? projectCommandsDir(opts2.cwd) : globalCommandsDir();
|
|
36291
37700
|
const filepath = `${dir}/${opts2.name}.md`;
|
|
@@ -36296,7 +37705,7 @@ async function saveCustomCommand(opts2) {
|
|
|
36296
37705
|
if (opts2.effort) data.effort = opts2.effort;
|
|
36297
37706
|
const frontmatter = serializeFrontmatter(data);
|
|
36298
37707
|
const content = frontmatter + opts2.template;
|
|
36299
|
-
await mkdir16(
|
|
37708
|
+
await mkdir16(dirname20(filepath), { recursive: true });
|
|
36300
37709
|
await writeFile15(filepath, content, "utf8");
|
|
36301
37710
|
return { filepath };
|
|
36302
37711
|
}
|