@lazyingart/agintiflow 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,135 @@
1
+ const COMPLEXITY_KEYWORDS = [
2
+ "architecture",
3
+ "refactor",
4
+ "debug",
5
+ "failing",
6
+ "test",
7
+ "implement",
8
+ "design",
9
+ "review",
10
+ "migrate",
11
+ "security",
12
+ "performance",
13
+ "multi-file",
14
+ "database",
15
+ "docker",
16
+ "ci",
17
+ "github",
18
+ ];
19
+
20
+ export const ROUTING_MODES = ["smart", "fast", "complex", "manual"];
21
+
22
+ export function getProviderDefaults(provider = "deepseek") {
23
+ if (provider === "openai") {
24
+ return {
25
+ provider: "openai",
26
+ apiKey: process.env.LLM_API_KEY || process.env.OPENAI_API_KEY || "",
27
+ baseURL: process.env.LLM_BASE_URL || "https://api.openai.com/v1",
28
+ model: process.env.OPENAI_DEFAULT_MODEL || process.env.LLM_MODEL || "gpt-5.4-mini",
29
+ };
30
+ }
31
+
32
+ return {
33
+ provider: "deepseek",
34
+ apiKey: process.env.LLM_API_KEY || process.env.DEEPSEEK_API_KEY || "",
35
+ baseURL: process.env.LLM_BASE_URL || "https://api.deepseek.com/v1",
36
+ model: process.env.DEEPSEEK_FAST_MODEL || process.env.LLM_MODEL || "deepseek-v4-flash",
37
+ };
38
+ }
39
+
40
+ export function getModelPresets() {
41
+ return {
42
+ fast: {
43
+ id: "fast",
44
+ label: "Fast base",
45
+ provider: "deepseek",
46
+ model: process.env.DEEPSEEK_FAST_MODEL || "deepseek-v4-flash",
47
+ description: "Default fast route for normal browser, shell, and short coding tasks.",
48
+ },
49
+ complex: {
50
+ id: "complex",
51
+ label: "Complex reasoning",
52
+ provider: "deepseek",
53
+ model: process.env.DEEPSEEK_PRO_MODEL || "deepseek-v4-pro",
54
+ description: "Higher-capacity DeepSeek route for multi-step coding and design tasks.",
55
+ },
56
+ codexPrimary: {
57
+ id: "codexPrimary",
58
+ label: "Codex primary wrapper",
59
+ provider: "codex-wrapper",
60
+ model: process.env.CODEX_PRIMARY_MODEL || "gpt-5.5",
61
+ reasoning: process.env.CODEX_PRIMARY_REASONING || "medium",
62
+ description: "External Codex wrapper route for coding enhancement tasks.",
63
+ },
64
+ codexSpare: {
65
+ id: "codexSpare",
66
+ label: "Codex spare wrapper",
67
+ provider: "codex-wrapper",
68
+ model: process.env.CODEX_SPARE_MODEL || "gpt-5.4-mini",
69
+ reasoning: process.env.CODEX_SPARE_REASONING || "high",
70
+ description: "Fallback Codex wrapper route when the primary wrapper fails.",
71
+ },
72
+ };
73
+ }
74
+
75
+ export function scoreTaskComplexity(goal = "") {
76
+ const text = String(goal).toLowerCase();
77
+ let score = text.length > 600 ? 2 : text.length > 240 ? 1 : 0;
78
+ for (const keyword of COMPLEXITY_KEYWORDS) {
79
+ if (text.includes(keyword)) score += 1;
80
+ }
81
+ return score;
82
+ }
83
+
84
+ export function normalizeRoutingMode(value) {
85
+ return ROUTING_MODES.includes(value) ? value : "smart";
86
+ }
87
+
88
+ export function selectModelRoute({ routingMode = "smart", provider = "deepseek", model = "", goal = "" } = {}) {
89
+ const mode = normalizeRoutingMode(routingMode);
90
+ const presets = getModelPresets();
91
+
92
+ if (mode === "manual") {
93
+ const defaults = getProviderDefaults(provider);
94
+ return {
95
+ routingMode: mode,
96
+ provider: defaults.provider,
97
+ model: model || defaults.model,
98
+ reason: "Manual provider/model selection.",
99
+ complexityScore: scoreTaskComplexity(goal),
100
+ };
101
+ }
102
+
103
+ if (mode === "complex") {
104
+ return {
105
+ routingMode: mode,
106
+ provider: presets.complex.provider,
107
+ model: presets.complex.model,
108
+ reason: "Complex route selected explicitly.",
109
+ complexityScore: scoreTaskComplexity(goal),
110
+ };
111
+ }
112
+
113
+ if (mode === "fast") {
114
+ return {
115
+ routingMode: mode,
116
+ provider: presets.fast.provider,
117
+ model: presets.fast.model,
118
+ reason: "Fast route selected explicitly.",
119
+ complexityScore: scoreTaskComplexity(goal),
120
+ };
121
+ }
122
+
123
+ const complexityScore = scoreTaskComplexity(goal);
124
+ const selected = complexityScore >= 3 ? presets.complex : presets.fast;
125
+ return {
126
+ routingMode: mode,
127
+ provider: selected.provider,
128
+ model: selected.model,
129
+ reason:
130
+ selected.id === "complex"
131
+ ? `Smart routing selected complex route; complexity score ${complexityScore}.`
132
+ : `Smart routing selected fast route; complexity score ${complexityScore}.`,
133
+ complexityScore,
134
+ };
135
+ }
@@ -0,0 +1,29 @@
1
+ const SECRET_PATTERNS = [
2
+ /\bsk-[A-Za-z0-9_-]{16,}\b/g,
3
+ /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g,
4
+ /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g,
5
+ /((?:api[_-]?key|token|secret|password|passwd|npm_token|_authToken)\s*[:=]\s*)[^\s"'`]+/gi,
6
+ /(\/\/registry\.npmjs\.org\/:_authToken=)[^\s"'`]+/gi,
7
+ /(Authorization:\s*Bearer\s+)[^\s"'`]+/gi,
8
+ ];
9
+
10
+ export function redactSensitiveText(value) {
11
+ let text = String(value ?? "");
12
+ for (const pattern of SECRET_PATTERNS) {
13
+ text = text.replace(pattern, (...args) => {
14
+ const captures = args.slice(1, -2).filter((item) => typeof item === "string");
15
+ const prefix = captures[0] || "";
16
+ return `${prefix}[REDACTED]`;
17
+ });
18
+ }
19
+ return text;
20
+ }
21
+
22
+ export function redactValue(value) {
23
+ if (typeof value === "string") return redactSensitiveText(value);
24
+ if (Array.isArray(value)) return value.map((item) => redactValue(item));
25
+ if (value && typeof value === "object") {
26
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactValue(item)]));
27
+ }
28
+ return value;
29
+ }
@@ -0,0 +1,73 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ export class SessionStore {
5
+ constructor(baseDir, sessionId) {
6
+ this.baseDir = baseDir;
7
+ this.sessionId = sessionId;
8
+ this.sessionDir = path.join(baseDir, sessionId);
9
+ this.artifactsDir = path.join(this.sessionDir, "artifacts");
10
+ this.statePath = path.join(this.sessionDir, "state.json");
11
+ this.planPath = path.join(this.sessionDir, "plan.md");
12
+ this.eventsPath = path.join(this.sessionDir, "events.jsonl");
13
+ this.storageStatePath = path.join(this.sessionDir, "storage-state.json");
14
+ }
15
+
16
+ async ensure() {
17
+ await fs.mkdir(this.artifactsDir, { recursive: true });
18
+ }
19
+
20
+ async loadState() {
21
+ try {
22
+ const raw = await fs.readFile(this.statePath, "utf8");
23
+ return JSON.parse(raw);
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ async saveState(state) {
30
+ await this.ensure();
31
+ await fs.writeFile(this.statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
32
+ }
33
+
34
+ async savePlan(planText) {
35
+ await this.ensure();
36
+ await fs.writeFile(this.planPath, `${planText.trim()}\n`, "utf8");
37
+ }
38
+
39
+ async appendEvent(type, data = {}) {
40
+ await this.ensure();
41
+ const line = JSON.stringify({
42
+ timestamp: new Date().toISOString(),
43
+ type,
44
+ data,
45
+ });
46
+ await fs.appendFile(this.eventsPath, `${line}\n`, "utf8");
47
+ }
48
+
49
+ async loadEvents() {
50
+ try {
51
+ const raw = await fs.readFile(this.eventsPath, "utf8");
52
+ return raw
53
+ .split("\n")
54
+ .map((line) => line.trim())
55
+ .filter(Boolean)
56
+ .map((line) => JSON.parse(line));
57
+ } catch {
58
+ return [];
59
+ }
60
+ }
61
+
62
+ async saveSnapshot(step, snapshot) {
63
+ await this.ensure();
64
+ const filename = `step-${String(step).padStart(3, "0")}.snapshot.json`;
65
+ const filePath = path.join(this.artifactsDir, filename);
66
+ await fs.writeFile(filePath, `${JSON.stringify(snapshot, null, 2)}\n`, "utf8");
67
+ return filePath;
68
+ }
69
+
70
+ screenshotPath(step) {
71
+ return path.join(this.artifactsDir, `step-${String(step).padStart(3, "0")}.png`);
72
+ }
73
+ }
@@ -0,0 +1,55 @@
1
+ export async function captureSnapshot(page, store, step) {
2
+ const snapshot = await page.evaluate(() => {
3
+ const isVisible = (el) => {
4
+ const rect = el.getBoundingClientRect();
5
+ const style = window.getComputedStyle(el);
6
+ return rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden";
7
+ };
8
+
9
+ document.querySelectorAll("[data-agent-id]").forEach((el) => el.removeAttribute("data-agent-id"));
10
+
11
+ const candidates = Array.from(
12
+ document.querySelectorAll("a, button, input, textarea, select, [role='button'], [contenteditable='true']")
13
+ );
14
+
15
+ const elements = [];
16
+
17
+ for (const el of candidates) {
18
+ if (!isVisible(el)) continue;
19
+ if (elements.length >= 50) break;
20
+
21
+ const id = String(elements.length + 1);
22
+ el.setAttribute("data-agent-id", id);
23
+
24
+ const tag = el.tagName.toLowerCase();
25
+ elements.push({
26
+ id,
27
+ tag,
28
+ role: el.getAttribute("role") || "",
29
+ text: (el.innerText || el.textContent || "").replace(/\s+/g, " ").trim().slice(0, 100),
30
+ ariaLabel: (el.getAttribute("aria-label") || "").trim().slice(0, 100),
31
+ placeholder: (el.getAttribute("placeholder") || "").trim().slice(0, 100),
32
+ href: tag === "a" ? (el.getAttribute("href") || "").trim() : "",
33
+ inputType: tag === "input" ? (el.getAttribute("type") || "text").trim() : "",
34
+ autocomplete: (el.getAttribute("autocomplete") || "").trim(),
35
+ });
36
+ }
37
+
38
+ return {
39
+ title: document.title,
40
+ url: window.location.href,
41
+ pageText: (document.body?.innerText || "").replace(/\s+/g, " ").trim().slice(0, 2500),
42
+ elements,
43
+ };
44
+ });
45
+
46
+ const screenshotPath = store.screenshotPath(step);
47
+ await page.screenshot({ path: screenshotPath, fullPage: true });
48
+ const snapshotPath = await store.saveSnapshot(step, snapshot);
49
+
50
+ return {
51
+ ...snapshot,
52
+ screenshotPath,
53
+ snapshotPath,
54
+ };
55
+ }
@@ -0,0 +1,193 @@
1
+ import { execFile as execFileCallback, execFileSync } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { getModelPresets } from "./model-routing.js";
4
+ import { redactSensitiveText } from "./redaction.js";
5
+
6
+ const execFile = promisify(execFileCallback);
7
+
8
+ export const WRAPPER_NAMES = ["codex", "claude", "gemini", "copilot", "qwen"];
9
+
10
+ const BASE_ADVISORY_PROMPT = [
11
+ "You are being called as an advisory wrapper tool inside AgInTiFlow.",
12
+ "Do not modify files, run destructive commands, push commits, install packages, or use secrets.",
13
+ "Return concise findings, commands to consider, or an implementation plan.",
14
+ ].join(" ");
15
+
16
+ function commandExists(command) {
17
+ try {
18
+ execFileSync("bash", ["-lc", `command -v ${command}`], { stdio: "ignore" });
19
+ return true;
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+
25
+ function cleanOutput(value, limit) {
26
+ return redactSensitiveText(value).trim().slice(0, limit);
27
+ }
28
+
29
+ function buildPrompt(prompt) {
30
+ return `${BASE_ADVISORY_PROMPT}\n\nTask:\n${prompt}`;
31
+ }
32
+
33
+ function codexArgs(prompt, config, preset) {
34
+ return [
35
+ "exec",
36
+ "--model",
37
+ preset.model,
38
+ "-c",
39
+ `model_reasoning_effort="${preset.reasoning}"`,
40
+ "--sandbox",
41
+ "read-only",
42
+ "--cd",
43
+ config.commandCwd,
44
+ "--skip-git-repo-check",
45
+ buildPrompt(prompt),
46
+ ];
47
+ }
48
+
49
+ function wrapperCommand(wrapper, prompt, config, { fallback = false } = {}) {
50
+ const presets = getModelPresets();
51
+ switch (wrapper) {
52
+ case "codex":
53
+ return {
54
+ command: "codex",
55
+ args: codexArgs(prompt, config, fallback ? presets.codexSpare : presets.codexPrimary),
56
+ };
57
+ case "claude":
58
+ return {
59
+ command: "claude",
60
+ args: [
61
+ "--print",
62
+ "--permission-mode",
63
+ "plan",
64
+ "--output-format",
65
+ "text",
66
+ "--model",
67
+ process.env.CLAUDE_WRAPPER_MODEL || "sonnet",
68
+ buildPrompt(prompt),
69
+ ],
70
+ };
71
+ case "gemini":
72
+ return {
73
+ command: "gemini",
74
+ args: ["--prompt", buildPrompt(prompt)],
75
+ };
76
+ case "copilot":
77
+ return {
78
+ command: "gh",
79
+ args: ["copilot", "-p", buildPrompt(prompt)],
80
+ };
81
+ case "qwen":
82
+ return {
83
+ command: "qwen",
84
+ args: ["--approval-mode", "plan", "--output-format", "text", buildPrompt(prompt)],
85
+ };
86
+ default:
87
+ return null;
88
+ }
89
+ }
90
+
91
+ export function isKnownWrapper(wrapper) {
92
+ return WRAPPER_NAMES.includes(wrapper);
93
+ }
94
+
95
+ export function listAgentWrappers() {
96
+ const presets = getModelPresets();
97
+ return [
98
+ {
99
+ name: "codex",
100
+ label: "Codex",
101
+ available: commandExists("codex"),
102
+ role: `Coding enhancement wrapper; primary ${presets.codexPrimary.model} ${presets.codexPrimary.reasoning}, spare ${presets.codexSpare.model} ${presets.codexSpare.reasoning}.`,
103
+ },
104
+ {
105
+ name: "claude",
106
+ label: "Claude Code",
107
+ available: commandExists("claude"),
108
+ role: "Planning and codebase reasoning wrapper in plan mode.",
109
+ },
110
+ {
111
+ name: "gemini",
112
+ label: "Gemini CLI",
113
+ available: commandExists("gemini"),
114
+ role: "General research and large-context CLI wrapper when installed.",
115
+ },
116
+ {
117
+ name: "copilot",
118
+ label: "GitHub Copilot CLI",
119
+ available: commandExists("gh"),
120
+ role: "GitHub/Copilot CLI wrapper when authenticated.",
121
+ },
122
+ {
123
+ name: "qwen",
124
+ label: "Qwen Code",
125
+ available: commandExists("qwen"),
126
+ role: "Chinese/open provider coding wrapper in plan approval mode.",
127
+ },
128
+ ];
129
+ }
130
+
131
+ export function wrapperStatusText() {
132
+ return listAgentWrappers()
133
+ .map((wrapper) => `${wrapper.name}:${wrapper.available ? "available" : "missing"}`)
134
+ .join(", ");
135
+ }
136
+
137
+ export async function runAgentWrapper({ wrapper, prompt }, config) {
138
+ if (!isKnownWrapper(wrapper)) {
139
+ return { ok: false, wrapper, error: `Unknown wrapper: ${wrapper}` };
140
+ }
141
+
142
+ const commandSpec = wrapperCommand(wrapper, prompt, config);
143
+ if (!commandSpec || !commandExists(commandSpec.command)) {
144
+ return { ok: false, wrapper, error: `Wrapper command is not available: ${wrapper}` };
145
+ }
146
+
147
+ const runOnce = async (spec) =>
148
+ execFile(spec.command, spec.args, {
149
+ cwd: config.commandCwd,
150
+ timeout: Number(config.wrapperTimeoutMs) || 120000,
151
+ maxBuffer: 512 * 1024,
152
+ env: process.env,
153
+ });
154
+
155
+ try {
156
+ const result = await runOnce(commandSpec);
157
+ return {
158
+ ok: true,
159
+ wrapper,
160
+ stdout: cleanOutput(result.stdout, 12000),
161
+ stderr: cleanOutput(result.stderr, 4000),
162
+ };
163
+ } catch (error) {
164
+ if (wrapper === "codex") {
165
+ const fallbackSpec = wrapperCommand(wrapper, prompt, config, { fallback: true });
166
+ try {
167
+ const fallback = await runOnce(fallbackSpec);
168
+ return {
169
+ ok: true,
170
+ wrapper,
171
+ fallback: true,
172
+ stdout: cleanOutput(fallback.stdout, 12000),
173
+ stderr: cleanOutput(fallback.stderr, 4000),
174
+ };
175
+ } catch (fallbackError) {
176
+ return {
177
+ ok: false,
178
+ wrapper,
179
+ error: redactSensitiveText(fallbackError instanceof Error ? fallbackError.message : String(fallbackError)),
180
+ primaryError: redactSensitiveText(error instanceof Error ? error.message : String(error)),
181
+ };
182
+ }
183
+ }
184
+
185
+ return {
186
+ ok: false,
187
+ wrapper,
188
+ error: redactSensitiveText(error instanceof Error ? error.message : String(error)),
189
+ stdout: cleanOutput(error.stdout, 8000),
190
+ stderr: cleanOutput(error.stderr, 4000),
191
+ };
192
+ }
193
+ }
package/src/web-db.js ADDED
@@ -0,0 +1,158 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { DatabaseSync } from "node:sqlite";
4
+ import { getModelPresets } from "./model-routing.js";
5
+
6
+ function defaultPreferences(baseDir) {
7
+ const presets = getModelPresets();
8
+ return {
9
+ routingMode: "smart",
10
+ provider: "deepseek",
11
+ model: presets.fast.model,
12
+ headless: true,
13
+ maxSteps: 15,
14
+ startUrl: "",
15
+ allowedDomains: "",
16
+ commandCwd: path.resolve(baseDir, ".."),
17
+ allowShellTool: true,
18
+ allowWrapperTools: false,
19
+ wrapperTimeoutMs: 120000,
20
+ sandboxMode: "docker-readonly",
21
+ packageInstallPolicy: "prompt",
22
+ useDockerSandbox: true,
23
+ dockerSandboxImage: "agintiflow-sandbox:latest",
24
+ allowPasswords: false,
25
+ allowDestructive: false,
26
+ language: "en",
27
+ };
28
+ }
29
+
30
+ export class WebDatabase {
31
+ constructor(baseDir) {
32
+ this.baseDir = baseDir;
33
+ this.dbDir = path.join(baseDir, ".sessions");
34
+ this.dbPath = path.join(this.dbDir, "web-state.sqlite");
35
+ fs.mkdirSync(this.dbDir, { recursive: true });
36
+ this.db = new DatabaseSync(this.dbPath);
37
+ this.db.exec(`
38
+ CREATE TABLE IF NOT EXISTS preferences (
39
+ key TEXT PRIMARY KEY,
40
+ value TEXT NOT NULL,
41
+ updated_at TEXT NOT NULL
42
+ );
43
+
44
+ CREATE TABLE IF NOT EXISTS sessions (
45
+ session_id TEXT PRIMARY KEY,
46
+ provider TEXT NOT NULL,
47
+ model TEXT NOT NULL,
48
+ goal TEXT NOT NULL,
49
+ status TEXT NOT NULL,
50
+ started_at TEXT NOT NULL,
51
+ updated_at TEXT NOT NULL,
52
+ ended_at TEXT,
53
+ result TEXT,
54
+ error TEXT
55
+ );
56
+ `);
57
+ }
58
+
59
+ getPreferences() {
60
+ const row = this.db.prepare("SELECT value FROM preferences WHERE key = ?").get("ui");
61
+ if (!row) return defaultPreferences(this.baseDir);
62
+
63
+ try {
64
+ return {
65
+ ...defaultPreferences(this.baseDir),
66
+ ...JSON.parse(row.value),
67
+ };
68
+ } catch {
69
+ return defaultPreferences(this.baseDir);
70
+ }
71
+ }
72
+
73
+ savePreferences(preferences) {
74
+ const value = JSON.stringify(preferences);
75
+ const updatedAt = new Date().toISOString();
76
+ this.db
77
+ .prepare(
78
+ `INSERT INTO preferences (key, value, updated_at)
79
+ VALUES (?, ?, ?)
80
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
81
+ )
82
+ .run("ui", value, updatedAt);
83
+ }
84
+
85
+ upsertSession(session) {
86
+ const updatedAt = session.updatedAt || new Date().toISOString();
87
+ this.db
88
+ .prepare(
89
+ `INSERT INTO sessions (
90
+ session_id, provider, model, goal, status, started_at, updated_at, ended_at, result, error
91
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
92
+ ON CONFLICT(session_id) DO UPDATE SET
93
+ provider = excluded.provider,
94
+ model = excluded.model,
95
+ goal = excluded.goal,
96
+ status = excluded.status,
97
+ updated_at = excluded.updated_at,
98
+ ended_at = excluded.ended_at,
99
+ result = excluded.result,
100
+ error = excluded.error`
101
+ )
102
+ .run(
103
+ session.sessionId,
104
+ session.provider,
105
+ session.model,
106
+ session.goal,
107
+ session.status,
108
+ session.startedAt,
109
+ updatedAt,
110
+ session.endedAt || null,
111
+ session.result || "",
112
+ session.error || ""
113
+ );
114
+ }
115
+
116
+ getSession(sessionId) {
117
+ return (
118
+ this.db
119
+ .prepare(
120
+ `SELECT
121
+ session_id AS sessionId,
122
+ provider,
123
+ model,
124
+ goal,
125
+ status,
126
+ started_at AS startedAt,
127
+ updated_at AS updatedAt,
128
+ ended_at AS endedAt,
129
+ result,
130
+ error
131
+ FROM sessions
132
+ WHERE session_id = ?`
133
+ )
134
+ .get(sessionId) || null
135
+ );
136
+ }
137
+
138
+ listSessions(limit = 20) {
139
+ return this.db
140
+ .prepare(
141
+ `SELECT
142
+ session_id AS sessionId,
143
+ provider,
144
+ model,
145
+ goal,
146
+ status,
147
+ started_at AS startedAt,
148
+ updated_at AS updatedAt,
149
+ ended_at AS endedAt,
150
+ result,
151
+ error
152
+ FROM sessions
153
+ ORDER BY updated_at DESC
154
+ LIMIT ?`
155
+ )
156
+ .all(limit);
157
+ }
158
+ }