206 lines
12 KiB
JavaScript
206 lines
12 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 MID_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 NEW_AGENTS_BROWSER = `- The only web tool is \`browser\`, the headed Jarvis Chromium window. \`web_search\`, \`web_fetch\`, and the other search tools are removed. Before a multi-step browse, \`read_file\` \`skills/browser/SKILL.md\`.
|
||
- \`browser\` actions: \`navigate\` (needs \`url\`; returns page text), \`snapshot\`, \`click\` (\`ref\` from the last snapshot), \`type\` (\`ref\` + \`text\`, optional \`submit\`), \`press\` (\`key\`), \`scroll\` (\`dy\`), \`wait\` (\`ms\`). To search, navigate to DuckDuckGo (\`https://duckduckgo.com/?q=QUERY\`), then click a result ref. Snapshot or navigate first. Refs change after every click. Do not use \`cu_observe\` or the shell for websites.`;
|
||
const OLD_AGENTS_KEEP_GOING = 'Keep going until the user’s request is fully complete. Never stop after announcing the next step. When the work is done, speak a short summary. A greeting does not need a long summary.';
|
||
const NEW_AGENTS_KEEP_GOING = 'The user’s request is the task. Finish it before you speak. Use the fewest tool calls that complete it. Never stop after announcing the next step, and never say you will overview or continue later. Only stop early if you are blocked, and then say what you found and what blocked you. When the work is done, speak the result. A greeting does not need a long summary.';
|
||
const OLD_AGENTS_MEMORY = `## Memory
|
||
|
||
- Daily log: \`memory/YYYY-MM-DD.md\` (append-only, concrete notes).
|
||
- Long-term: \`MEMORY.md\` for durable facts, decisions, open loops.
|
||
- User model: \`USER.md\` for stable preferences (dated active / superseded directives).
|
||
- Before writing a memory file, read it. Never write empty placeholders.
|
||
- Avoid secrets unless the user explicitly asks to store one.
|
||
- Writes to these workspace markdown files do not wait for confirmation.`;
|
||
const NEW_AGENTS_MEMORY = `## Memory
|
||
|
||
You are not sentient. These files are your cortex. You own them and you keep them yourself.
|
||
|
||
- Daily log: \`memory/YYYY-MM-DD.md\` (append-only, concrete notes).
|
||
- Long-term: \`MEMORY.md\` for durable facts, decisions, open loops.
|
||
- User model: \`USER.md\` for stable preferences (dated active / superseded directives).
|
||
- On any turn that teaches a durable fact, preference, name, decision, outcome, or open loop, write it before the final reply. Do not ask. Do not wait to be told to remember.
|
||
- Search first and update the existing note. Read before you replace. Never write empty placeholders.
|
||
- If the \`obsidian\` tool is registered, it is the only vault tool. Call \`obsidian\` with \`action\` set to \`memory_search\`, \`read\`, \`write\`, \`list\`, \`search\`, \`mkdir\`, \`move\`, \`delete\`, \`trash\`, \`restore\`, or \`status\`. Do not invent \`obsidian_write\` or call \`memory_search\` as its own tool. Otherwise use these files, or \`memory_write\` / \`memory_remember\`.
|
||
- Avoid secrets unless the user explicitly asks to store one.
|
||
- Writes to these workspace markdown files, memory notes, and the agent vault do not wait for confirmation.`;
|
||
const STARTPAGE_AGENTS_BROWSER = NEW_AGENTS_BROWSER.replace(
|
||
'To search, navigate to DuckDuckGo (`https://duckduckgo.com/?q=QUERY`), then click a result ref. ',
|
||
'To search, navigate to Startpage (`https://www.startpage.com/sp/search?query=QUERY&cat=web&language=english&lui=english&t=device&abe=1&abd=1&abp=1`), then click a result ref. Do not use DuckDuckGo or Google. '
|
||
);
|
||
const PREV_AGENTS_BROWSER = NEW_AGENTS_BROWSER.replace(
|
||
'To search, navigate to DuckDuckGo (`https://duckduckgo.com/?q=QUERY`), then click a result ref. ',
|
||
'To search, navigate to a public search url, then click a result ref. '
|
||
);
|
||
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 MID_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.`;
|
||
const NEW_TOOLS_BROWSER = `- Public HTTP via curl or wget is blocked. Use \`browser\` in the Jarvis Chromium window.
|
||
|
||
## Browser
|
||
|
||
- \`browser\` is the only web tool. It drives one headed Playwright Chromium window.
|
||
- \`web_search\`, \`google_search\`, \`fetch_page\`, \`web_fetch\`, \`wiki_search\`, \`hn_search\`, and \`code_search\` are removed. Do not call them.
|
||
- To search, \`navigate\` to \`https://duckduckgo.com/?q=QUERY\`, then \`click\` a result ref.
|
||
- \`navigate\` and \`snapshot\` return page \`text\`. Read it, then decide the next action.
|
||
- Cookie walls, forms, leftover challenges: \`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 rewriteBrowserSkill(dir) {
|
||
const file = path.join(dir, 'skills/browser/SKILL.md');
|
||
const template = path.join(TEMPLATE_DIR, 'skills/browser/SKILL.md');
|
||
try {
|
||
const text = fs.readFileSync(file, 'utf8');
|
||
if (!/web_search|fetch_page|web_fetch|startpage\.com/.test(text) && /duckduckgo\.com\/\?q/.test(text) && /window stays open/.test(text)) return false;
|
||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||
fs.writeFileSync(file, fs.readFileSync(template, 'utf8'));
|
||
return true;
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
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, 'AGENTS.md'), MID_AGENTS_BROWSER, NEW_AGENTS_BROWSER);
|
||
replaceOnce(path.join(dest, 'AGENTS.md'), PREV_AGENTS_BROWSER, NEW_AGENTS_BROWSER);
|
||
replaceOnce(path.join(dest, 'AGENTS.md'), STARTPAGE_AGENTS_BROWSER, NEW_AGENTS_BROWSER);
|
||
replaceOnce(path.join(dest, 'AGENTS.md'), OLD_AGENTS_MEMORY, NEW_AGENTS_MEMORY);
|
||
replaceOnce(
|
||
path.join(dest, 'AGENTS.md'),
|
||
'- If the `obsidian` tool is registered, the vault is the durable store. Write there without confirmation. Otherwise use these files, or `memory_write` / `memory_remember`.',
|
||
'- If the `obsidian` tool is registered, it is the only vault tool. Call `obsidian` with `action` set to `memory_search`, `read`, `write`, `list`, `search`, `mkdir`, `move`, `delete`, `trash`, `restore`, or `status`. Do not invent `obsidian_write` or call `memory_search` as its own tool. Otherwise use these files, or `memory_write` / `memory_remember`.',
|
||
);
|
||
replaceOnce(path.join(dest, 'AGENTS.md'), OLD_AGENTS_KEEP_GOING, NEW_AGENTS_KEEP_GOING);
|
||
replaceOnce(path.join(dest, 'TOOLS.md'), OLD_TOOLS_BROWSER, NEW_TOOLS_BROWSER);
|
||
replaceOnce(path.join(dest, 'TOOLS.md'), MID_TOOLS_BROWSER, NEW_TOOLS_BROWSER);
|
||
replaceOnce(
|
||
path.join(dest, 'TOOLS.md'),
|
||
'- To search, `navigate` to a public search url, then `click` a result ref.',
|
||
'- To search, `navigate` to `https://duckduckgo.com/?q=QUERY`, then `click` a result ref.',
|
||
);
|
||
replaceOnce(
|
||
path.join(dest, 'TOOLS.md'),
|
||
'- To search, `navigate` to `https://www.startpage.com/sp/search?query=QUERY&cat=web&language=english&lui=english&t=device&abe=1&abd=1&abp=1`, then `click` a result ref. Do not use DuckDuckGo or Google.',
|
||
'- To search, `navigate` to `https://duckduckgo.com/?q=QUERY`, then `click` a result ref.',
|
||
);
|
||
rewriteBrowserSkill(dest);
|
||
applyAssistantName(dest, name);
|
||
applyAssistantPrompt(dest, prompt);
|
||
return dest;
|
||
}
|