126 lines
5.5 KiB
JavaScript
126 lines
5.5 KiB
JavaScript
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
|
|
const TEMPLATE_DIR = path.resolve(new URL('../vendor/agent-harness/agent-workspace', import.meta.url).pathname);
|
|
const SEED_FILES = [
|
|
'SOUL.md',
|
|
'IDENTITY.md',
|
|
'AGENTS.md',
|
|
'USER.md',
|
|
'MEMORY.md',
|
|
'BOOTSTRAP.md',
|
|
'HEARTBEAT.md',
|
|
'TOOLS.md',
|
|
'PERSONA.md',
|
|
'skills/skill-creator/SKILL.md',
|
|
'skills/browser/SKILL.md',
|
|
];
|
|
|
|
export function normalizeAssistantName(value) {
|
|
const text = String(value || '').replace(/[\r\n\t]/g, ' ').trim().replace(/\s+/g, ' ').slice(0, 32);
|
|
if (!text || /[<>&]/.test(text)) return 'Jarvis';
|
|
return text;
|
|
}
|
|
|
|
export function agentWorkspaceDir() {
|
|
return path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local/share'), 'jarvis/workspace');
|
|
}
|
|
|
|
export function templateWorkspaceDir() {
|
|
return TEMPLATE_DIR;
|
|
}
|
|
|
|
function copyIfMissing(from, to) {
|
|
if (fs.existsSync(to)) return false;
|
|
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
fs.copyFileSync(from, to);
|
|
return true;
|
|
}
|
|
|
|
const OLD_AGENTS_BROWSER = '- `web_search` / `google_search` / `fetch_page` / `web_fetch` / `wiki_search` / `hn_search` / `code_search` — Playwright Chromium. For cookie walls or extra clicks, call `browser` with snapshot then click or type.';
|
|
const NEW_AGENTS_BROWSER = `- Web tools share one headed Playwright Chromium window. \`web_search\` / \`google_search\` / \`wiki_search\` / \`hn_search\` / \`code_search\` find links. \`fetch_page\` / \`web_fetch\` read a public page. For cookie banners, forms, logins, or leftover challenges, call \`browser\`. Before a multi-step browse, \`read_file\` \`skills/browser/SKILL.md\`.
|
|
- \`browser\` actions: \`navigate\` (needs \`url\`), \`snapshot\`, \`click\` (\`ref\` from the last snapshot), \`type\` (\`ref\` + \`text\`, optional \`submit\`), \`press\` (\`key\`), \`scroll\` (\`dy\`), \`wait\` (\`ms\`). Snapshot or navigate first. Refs change after every click. Do not use \`cu_observe\` or the shell for websites.`;
|
|
const OLD_TOOLS_BROWSER = '- Public HTTP via curl or wget is blocked. Use `web_search` / `web_fetch` in the Jarvis Chromium window. For cookie walls, call `browser` with snapshot then click or type.';
|
|
const NEW_TOOLS_BROWSER = `- Public HTTP via curl or wget is blocked. Use \`web_search\` / \`web_fetch\` in the Jarvis Chromium window.
|
|
|
|
## Browser
|
|
|
|
- Search, fetch, and \`browser\` share one headed Playwright Chromium window.
|
|
- \`web_search\` finds links. \`fetch_page\` reads a public page.
|
|
- Cookie walls, forms, leftover challenges: call \`browser\`. \`read_file\` \`skills/browser/SKILL.md\` for the playbook.
|
|
- \`browser\` actions: \`navigate\` + \`url\`, \`snapshot\`, \`click\`/\`type\` with \`ref\` from the last snapshot, \`press\` + \`key\`, \`scroll\` + \`dy\`, \`wait\` + \`ms\`.
|
|
- Snapshot or navigate before every click or type. Refs go stale after a click.
|
|
- Do not use \`cu_observe\` or the shell for websites.`;
|
|
|
|
function replaceOnce(file, from, to) {
|
|
try {
|
|
const text = fs.readFileSync(file, 'utf8');
|
|
if (!from || !text.includes(from)) return false;
|
|
fs.writeFileSync(file, text.replace(from, to));
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function escapeRegExp(value) {
|
|
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
}
|
|
|
|
function renameAssistant(text, previous, next) {
|
|
if (!previous || previous === next) return text;
|
|
return String(text || '').replace(new RegExp(escapeRegExp(previous), 'g'), next);
|
|
}
|
|
|
|
export function applyAssistantName(dir, name) {
|
|
const who = normalizeAssistantName(name);
|
|
const identity = path.join(dir, 'IDENTITY.md');
|
|
let text = '';
|
|
try { text = fs.readFileSync(identity, 'utf8'); } catch { text = '# IDENTITY.md\n\n'; }
|
|
const previous = (text.match(/\*\*Name:\*\*\s*(.+)/) || [])[1]?.trim() || 'Jarvis';
|
|
text = renameAssistant(text, previous, who);
|
|
if (/\*\*Name:\*\*/.test(text)) text = text.replace(/\*\*Name:\*\*.*/, `**Name:** ${who}`);
|
|
else text += `\n- **Name:** ${who}\n`;
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
fs.writeFileSync(identity, text);
|
|
const soul = path.join(dir, 'SOUL.md');
|
|
try {
|
|
let soulText = renameAssistant(fs.readFileSync(soul, 'utf8'), previous, who);
|
|
soulText = soulText.replace(/^# SOUL\.md:.*/m, `# SOUL.md: ${who}`);
|
|
fs.writeFileSync(soul, soulText);
|
|
} catch {}
|
|
const memory = path.join(dir, 'MEMORY.md');
|
|
try {
|
|
let mem = renameAssistant(fs.readFileSync(memory, 'utf8'), previous, who);
|
|
if (/Assistant name:/.test(mem)) mem = mem.replace(/Assistant name:.*/, `Assistant name: ${who}`);
|
|
fs.writeFileSync(memory, mem);
|
|
} catch {}
|
|
return who;
|
|
}
|
|
|
|
export function applyAssistantPrompt(dir, prompt) {
|
|
const notes = String(prompt || '').replace(/\0/g, '').replace(/\s+$/g, '').slice(0, 4000);
|
|
const file = path.join(dir, 'PERSONA.md');
|
|
const body = notes
|
|
? `# PERSONA.md\n\n${notes}\n`
|
|
: '# PERSONA.md\n\n';
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
fs.writeFileSync(file, body);
|
|
return notes;
|
|
}
|
|
|
|
export function ensureAgentWorkspace({ name = 'Jarvis', prompt = '' } = {}) {
|
|
const dest = agentWorkspaceDir();
|
|
fs.mkdirSync(path.join(dest, 'memory'), { recursive: true });
|
|
fs.mkdirSync(path.join(dest, 'skills'), { recursive: true });
|
|
for (const rel of SEED_FILES) {
|
|
copyIfMissing(path.join(TEMPLATE_DIR, rel), path.join(dest, rel));
|
|
}
|
|
replaceOnce(path.join(dest, 'AGENTS.md'), OLD_AGENTS_BROWSER, NEW_AGENTS_BROWSER);
|
|
replaceOnce(path.join(dest, 'TOOLS.md'), OLD_TOOLS_BROWSER, NEW_TOOLS_BROWSER);
|
|
applyAssistantName(dest, name);
|
|
applyAssistantPrompt(dest, prompt);
|
|
return dest;
|
|
}
|