feat(FN-2981): add droid-cli auth and status routes with experimental agent
This merge lands v0.12.0 with two major features: a droid-cli provider integration adding auth routes, status endpoints, and a settings toggle hook for controlling CLI-based authentication, plus a new experimental agent onboarding modal with a create-agent form. The release also stabilizes engine st Fusion-Task-Id: FN-2981
This commit is contained in:
@@ -131,7 +131,7 @@ describe("Database", () => {
|
||||
});
|
||||
|
||||
it("seeds schema version", () => {
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
expect(db.getSchemaVersion()).toBe(56);
|
||||
});
|
||||
|
||||
it("seeds lastModified", () => {
|
||||
@@ -154,7 +154,7 @@ describe("Database", () => {
|
||||
|
||||
it("is idempotent - calling init() twice does not fail", () => {
|
||||
expect(() => db.init()).not.toThrow();
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
expect(db.getSchemaVersion()).toBe(56);
|
||||
});
|
||||
|
||||
it("does not overwrite existing config on re-init", () => {
|
||||
@@ -761,7 +761,7 @@ describe("schema migrations", () => {
|
||||
db.init();
|
||||
|
||||
// Verify version bumped to 29 (includes v1→v2 through v26→v29)
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
expect(db.getSchemaVersion()).toBe(56);
|
||||
|
||||
// Verify new columns exist and existing data is intact
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
@@ -786,11 +786,11 @@ describe("schema migrations", () => {
|
||||
const db = new Database(fusionDir);
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
expect(db.getSchemaVersion()).toBe(56);
|
||||
|
||||
// Re-init should not fail
|
||||
db.init();
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
expect(db.getSchemaVersion()).toBe(56);
|
||||
|
||||
db.close();
|
||||
});
|
||||
@@ -825,7 +825,7 @@ describe("schema migrations", () => {
|
||||
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
expect(db.getSchemaVersion()).toBe(56);
|
||||
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
expect(cols.map((col) => col.name)).toContain("priority");
|
||||
@@ -866,7 +866,7 @@ describe("schema migrations", () => {
|
||||
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
expect(db.getSchemaVersion()).toBe(56);
|
||||
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
const colNames = cols.map((col) => col.name);
|
||||
@@ -935,7 +935,7 @@ describe("schema migrations", () => {
|
||||
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
expect(db.getSchemaVersion()).toBe(56);
|
||||
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
const colNames = cols.map((col) => col.name);
|
||||
@@ -1038,7 +1038,7 @@ describe("schema migrations", () => {
|
||||
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
expect(db.getSchemaVersion()).toBe(56);
|
||||
|
||||
const cols = db.prepare("PRAGMA table_info(chat_messages)").all() as Array<{ name: string }>;
|
||||
expect(cols.map((col) => col.name)).toContain("attachments");
|
||||
@@ -1112,7 +1112,7 @@ describe("schema migrations", () => {
|
||||
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
expect(db.getSchemaVersion()).toBe(56);
|
||||
|
||||
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'agentRatings'").all() as Array<{ name: string }>;
|
||||
expect(tables).toEqual([{ name: "agentRatings" }]);
|
||||
@@ -1136,7 +1136,7 @@ describe("schema migrations", () => {
|
||||
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
expect(db.getSchemaVersion()).toBe(56);
|
||||
|
||||
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'mission_events'").all() as Array<{ name: string }>;
|
||||
expect(tables).toEqual([{ name: "mission_events" }]);
|
||||
@@ -1240,7 +1240,7 @@ describe("schema migrations", () => {
|
||||
db.init();
|
||||
|
||||
// Verify version bumped to 29
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
expect(db.getSchemaVersion()).toBe(56);
|
||||
|
||||
// Verify new columns exist and existing data is intact
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
@@ -1691,7 +1691,7 @@ describe("createDatabase factory", () => {
|
||||
const db = createDatabase(fusionDir);
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
expect(db.getSchemaVersion()).toBe(56);
|
||||
expect(db.getLastModified()).toBeGreaterThan(0);
|
||||
|
||||
db.close();
|
||||
|
||||
@@ -779,7 +779,7 @@ describe("Migration: pre-33 DB upgrade", () => {
|
||||
// Step 1: Create a fresh database at v33 (runs all migrations up to 33)
|
||||
const db1 = createDatabase(legacyDir);
|
||||
db1.init();
|
||||
expect(db1.getSchemaVersion()).toBe(55);
|
||||
expect(db1.getSchemaVersion()).toBe(56);
|
||||
db1.close();
|
||||
|
||||
// Step 2: Manually downgrade to version 32 and drop insight tables
|
||||
@@ -814,7 +814,7 @@ describe("Migration: pre-33 DB upgrade", () => {
|
||||
expect(tableNamesBefore).not.toContain("project_insight_runs");
|
||||
// Now run init — this triggers the v32→v33 migration
|
||||
db3.init();
|
||||
expect(db3.getSchemaVersion()).toBe(55);
|
||||
expect(db3.getSchemaVersion()).toBe(56);
|
||||
|
||||
// Step 4: Verify insight tables exist after migration
|
||||
const tablesAfter = db3.prepare(
|
||||
@@ -845,12 +845,12 @@ describe("Migration: pre-33 DB upgrade", () => {
|
||||
try {
|
||||
const db1 = createDatabase(testDir);
|
||||
db1.init();
|
||||
expect(db1.getSchemaVersion()).toBe(55);
|
||||
expect(db1.getSchemaVersion()).toBe(56);
|
||||
db1.close();
|
||||
|
||||
const db2 = createDatabase(testDir);
|
||||
expect(() => db2.init()).not.toThrow();
|
||||
expect(db2.getSchemaVersion()).toBe(55);
|
||||
expect(db2.getSchemaVersion()).toBe(56);
|
||||
db2.close();
|
||||
} finally {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
|
||||
@@ -2629,7 +2629,7 @@ describe("MissionStore", () => {
|
||||
|
||||
describe("Loop State & Validator Run Schema (v31)", () => {
|
||||
it("schema version is 40 after migration", () => {
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
expect(db.getSchemaVersion()).toBe(56);
|
||||
});
|
||||
|
||||
it("mission_features table has loop state columns", () => {
|
||||
|
||||
@@ -742,7 +742,7 @@ describe("RoadmapStore", () => {
|
||||
|
||||
describe("schema version", () => {
|
||||
it("schema version is 40 after init", () => {
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
expect(db.getSchemaVersion()).toBe(56);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -465,7 +465,7 @@ describe("Run Audit", () => {
|
||||
});
|
||||
|
||||
it("schema version is bumped to 40", () => {
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
expect(db.getSchemaVersion()).toBe(56);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,7 +51,7 @@ describe("TaskStore task documents", () => {
|
||||
|
||||
expect(tableNames.has("task_documents")).toBe(true);
|
||||
expect(tableNames.has("task_document_revisions")).toBe(true);
|
||||
expect(db.getSchemaVersion()).toBe(55);
|
||||
expect(db.getSchemaVersion()).toBe(56);
|
||||
|
||||
const index = db
|
||||
.prepare(
|
||||
|
||||
@@ -56,6 +56,7 @@ interface ChatSessionRow {
|
||||
modelId: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
cliSessionFile: string | null;
|
||||
}
|
||||
|
||||
/** Database row shape for chat_messages. */
|
||||
@@ -97,6 +98,7 @@ export class ChatStore extends EventEmitter<ChatStoreEvents> {
|
||||
modelId: row.modelId ?? null,
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
cliSessionFile: row.cliSessionFile ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -138,6 +140,7 @@ export class ChatStore extends EventEmitter<ChatStoreEvents> {
|
||||
modelId: input.modelId ?? null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
cliSessionFile: null,
|
||||
};
|
||||
|
||||
this.db.prepare(`
|
||||
@@ -331,6 +334,24 @@ export class ChatStore extends EventEmitter<ChatStoreEvents> {
|
||||
return this.updateSession(id, { status: "archived" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the pi/Claude CLI session file path for a chat. Called once,
|
||||
* after the SessionManager for the chat first creates its on-disk file,
|
||||
* so subsequent turns can reopen it via SessionManager.open.
|
||||
*
|
||||
* Does not bump updatedAt or emit events — this is internal plumbing,
|
||||
* not a user-visible state change.
|
||||
*
|
||||
* @param id - Session ID
|
||||
* @param cliSessionFile - Absolute path to the session file, or null to clear
|
||||
*/
|
||||
setCliSessionFile(id: string, cliSessionFile: string | null): void {
|
||||
this.db
|
||||
.prepare("UPDATE chat_sessions SET cliSessionFile = ? WHERE id = ?")
|
||||
.run(cliSessionFile, id);
|
||||
this.db.bumpLastModified();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a chat session and all its messages.
|
||||
* Messages are cascade-deleted via foreign key constraint.
|
||||
|
||||
@@ -37,6 +37,14 @@ export interface ChatSession {
|
||||
createdAt: string;
|
||||
/** When the session was last updated */
|
||||
updatedAt: string;
|
||||
/**
|
||||
* Absolute path to the pi/Claude CLI session file backing this chat, if
|
||||
* any. Set on the first assistant turn (when SessionManager.create
|
||||
* provisions the file) and reused by SessionManager.open on subsequent
|
||||
* turns so the on-disk CLI session is resumed instead of recreated. Null
|
||||
* for sessions that have never produced an assistant reply.
|
||||
*/
|
||||
cliSessionFile: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2115,6 +2115,17 @@ export class Database {
|
||||
});
|
||||
}
|
||||
|
||||
// Persist the pi/Claude CLI session file path per chat so quick-chat
|
||||
// turns reuse the same on-disk session instead of starting fresh each
|
||||
// user message.
|
||||
if (version < 56) {
|
||||
this.applyMigration(56, () => {
|
||||
if (this.hasTable("chat_sessions")) {
|
||||
this.addColumnIfMissing("chat_sessions", "cliSessionFile", "TEXT");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -41,6 +41,7 @@ export const DEFAULT_GLOBAL_SETTINGS = {
|
||||
showGitHubStarButton: true,
|
||||
modelOnboardingComplete: undefined,
|
||||
useClaudeCli: undefined,
|
||||
useDroidCli: undefined,
|
||||
// Global baseline lanes for per-role model selection
|
||||
executionGlobalProvider: undefined,
|
||||
executionGlobalModelId: undefined,
|
||||
|
||||
@@ -1349,6 +1349,13 @@ export interface GlobalSettings {
|
||||
* array in the agent settings for `"npm:pi-claude-cli"` (legacy signal).
|
||||
* Setting this field explicitly (true/false) always wins. */
|
||||
useClaudeCli?: boolean;
|
||||
/** When true, route Factory AI model calls through the locally-installed Droid CLI
|
||||
* via the `droid-cli` provider path (instead of direct API provider calls).
|
||||
*
|
||||
* When left undefined, Droid CLI routing stays disabled unless explicitly enabled
|
||||
* by the dashboard auth toggle. Setting this field explicitly (true/false)
|
||||
* always wins. */
|
||||
useDroidCli?: boolean;
|
||||
/** Global baseline AI model provider for task execution (executor agent).
|
||||
* This is the global lane that project-level `executionProvider` can override.
|
||||
* Must be set together with `executionGlobalModelId`. Falls back to
|
||||
|
||||
Reference in New Issue
Block a user