upfynai-code 2.8.3 → 2.8.4

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,67 @@
1
+ /**
2
+ * Gitagent Agent
3
+ * Handles: gitagent-detect, gitagent-parse
4
+ *
5
+ * Relay-compatible actions for detecting and parsing gitagent directory structures
6
+ * on the user's local machine.
7
+ */
8
+ import fs from 'fs';
9
+ import { promises as fsp } from 'fs';
10
+ import path from 'path';
11
+ import { detectGitagent, parseGitagentRepo } from '../gitagent/parser.js';
12
+
13
+ /** fs-backed helpers injected into the parser */
14
+ async function fsFileExists(filePath) {
15
+ try {
16
+ await fsp.access(filePath, fs.constants.F_OK);
17
+ return true;
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+
23
+ async function fsReadFile(filePath) {
24
+ try {
25
+ return await fsp.readFile(filePath, 'utf8');
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ async function fsListDir(dirPath) {
32
+ try {
33
+ return await fsp.readdir(dirPath);
34
+ } catch {
35
+ return [];
36
+ }
37
+ }
38
+
39
+ export default {
40
+ name: 'gitagent',
41
+ actions: {
42
+ /**
43
+ * Lightweight check for agent.yaml existence.
44
+ * @param {{ projectPath: string }} params
45
+ * @returns {{ detected: boolean }}
46
+ */
47
+ 'gitagent-detect': async (params) => {
48
+ const projectPath = params.projectPath;
49
+ if (!projectPath) return { detected: false };
50
+ const detected = await detectGitagent(projectPath, fsFileExists);
51
+ return { detected };
52
+ },
53
+
54
+ /**
55
+ * Full parse — returns the complete GitagentDefinition as JSON.
56
+ * @param {{ projectPath: string }} params
57
+ * @returns {{ definition: object|null }}
58
+ */
59
+ 'gitagent-parse': async (params) => {
60
+ const projectPath = params.projectPath;
61
+ if (!projectPath) return { definition: null };
62
+
63
+ const definition = await parseGitagentRepo(projectPath, fsReadFile, fsListDir);
64
+ return { definition };
65
+ },
66
+ },
67
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Gitagent — Re-exports
3
+ *
4
+ * Usage:
5
+ * import { detectGitagent, parseGitagentRepo } from 'upfynai-shared/gitagent';
6
+ * import { buildSystemPromptAppendix, mapGitagentModel } from 'upfynai-shared/gitagent';
7
+ */
8
+ export { detectGitagent, parseGitagentRepo } from './parser.js';
9
+ export { buildSystemPromptAppendix, mapGitagentModel, extractAllowedTools } from './prompt-builder.js';
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Gitagent Parser
3
+ *
4
+ * Reads a project directory following the gitagent spec (https://github.com/open-gitagent/gitagent)
5
+ * and produces a GitagentDefinition object. All file I/O is dependency-injected so the parser
6
+ * works for both local fs and relay file-read paths.
7
+ *
8
+ * @typedef {Object} GitagentDefinition
9
+ * @property {Object} manifest - Parsed agent.yaml
10
+ * @property {string|null} soul - SOUL.md content
11
+ * @property {string|null} rules - RULES.md content
12
+ * @property {string|null} agents - AGENTS.md content
13
+ * @property {Array} skills - Parsed skill definitions
14
+ * @property {Array} tools - Parsed tool schemas
15
+ * @property {Object|null} knowledge - Knowledge index + always-loaded docs
16
+ * @property {string|null} memory - MEMORY.md content
17
+ */
18
+
19
+ import yaml from 'js-yaml';
20
+
21
+ /**
22
+ * Lightweight check: does agent.yaml exist in the project root?
23
+ * @param {string} projectPath - Absolute path to the project
24
+ * @param {(filePath: string) => Promise<boolean>} fileExists - Injected existence check
25
+ * @returns {Promise<boolean>}
26
+ */
27
+ export async function detectGitagent(projectPath, fileExists) {
28
+ try {
29
+ return await fileExists(join(projectPath, 'agent.yaml'));
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Full parse of a gitagent directory into a definition object.
37
+ * @param {string} projectPath
38
+ * @param {(filePath: string) => Promise<string|null>} readFile - Returns file content or null
39
+ * @param {(dirPath: string) => Promise<string[]>} listDir - Returns entry names or []
40
+ * @returns {Promise<GitagentDefinition|null>}
41
+ */
42
+ export async function parseGitagentRepo(projectPath, readFile, listDir) {
43
+ // Read manifest — required
44
+ const agentYamlContent = await readFile(join(projectPath, 'agent.yaml'));
45
+ if (!agentYamlContent) return null;
46
+
47
+ let manifest;
48
+ try {
49
+ manifest = yaml.load(agentYamlContent);
50
+ } catch {
51
+ return null;
52
+ }
53
+ if (!manifest || typeof manifest !== 'object') return null;
54
+
55
+ // Read core markdown files (all optional)
56
+ const [soul, rules, agents, memory] = await Promise.all([
57
+ safeRead(readFile, join(projectPath, 'SOUL.md')),
58
+ safeRead(readFile, join(projectPath, 'RULES.md')),
59
+ safeRead(readFile, join(projectPath, 'AGENTS.md')),
60
+ safeRead(readFile, join(projectPath, 'memory', 'MEMORY.md')),
61
+ ]);
62
+
63
+ // Parse skills
64
+ const skills = await parseSkills(projectPath, manifest.skills, readFile, listDir);
65
+
66
+ // Parse tools
67
+ const tools = await parseTools(projectPath, manifest.tools, readFile, listDir);
68
+
69
+ // Parse knowledge
70
+ const knowledge = await parseKnowledge(projectPath, readFile);
71
+
72
+ return { manifest, soul, rules, agents, skills, tools, knowledge, memory };
73
+ }
74
+
75
+ // ── Internal helpers ────────────────────────────────────────────────
76
+
77
+ function join(...parts) {
78
+ // Simple platform-agnostic join (works with / on all platforms in Node)
79
+ return parts.join('/').replace(/\/+/g, '/');
80
+ }
81
+
82
+ async function safeRead(readFile, filePath) {
83
+ try {
84
+ return await readFile(filePath) || null;
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Parse skills from the skills/ directory.
92
+ * Each skill is a subfolder containing SKILL.md with optional YAML frontmatter.
93
+ */
94
+ async function parseSkills(projectPath, declaredSkills, readFile, listDir) {
95
+ const skillsDir = join(projectPath, 'skills');
96
+ let entries;
97
+ try {
98
+ entries = await listDir(skillsDir);
99
+ } catch {
100
+ return [];
101
+ }
102
+ if (!entries || entries.length === 0) return [];
103
+
104
+ const skills = [];
105
+ for (const entry of entries) {
106
+ const skillMd = await safeRead(readFile, join(skillsDir, entry, 'SKILL.md'));
107
+ if (!skillMd) continue;
108
+
109
+ const { frontmatter, body } = parseFrontmatter(skillMd);
110
+ skills.push({
111
+ name: entry,
112
+ ...(frontmatter || {}),
113
+ description: body || null,
114
+ });
115
+ }
116
+ return skills;
117
+ }
118
+
119
+ /**
120
+ * Parse tool definitions from tools/ directory.
121
+ * Each tool is a YAML file: tools/{name}.yaml
122
+ */
123
+ async function parseTools(projectPath, declaredTools, readFile, listDir) {
124
+ const toolsDir = join(projectPath, 'tools');
125
+ let entries;
126
+ try {
127
+ entries = await listDir(toolsDir);
128
+ } catch {
129
+ return [];
130
+ }
131
+ if (!entries || entries.length === 0) return [];
132
+
133
+ const tools = [];
134
+ for (const entry of entries) {
135
+ if (!entry.endsWith('.yaml') && !entry.endsWith('.yml')) continue;
136
+ const content = await safeRead(readFile, join(toolsDir, entry));
137
+ if (!content) continue;
138
+ try {
139
+ const schema = yaml.load(content);
140
+ if (schema && typeof schema === 'object') {
141
+ tools.push({ name: entry.replace(/\.ya?ml$/, ''), ...schema });
142
+ }
143
+ } catch {
144
+ // skip invalid tool yaml
145
+ }
146
+ }
147
+ return tools;
148
+ }
149
+
150
+ /**
151
+ * Parse knowledge index and load always_load documents.
152
+ */
153
+ async function parseKnowledge(projectPath, readFile) {
154
+ const indexContent = await safeRead(readFile, join(projectPath, 'knowledge', 'index.yaml'));
155
+ if (!indexContent) return null;
156
+
157
+ let index;
158
+ try {
159
+ index = yaml.load(indexContent);
160
+ } catch {
161
+ return null;
162
+ }
163
+ if (!index || typeof index !== 'object') return null;
164
+
165
+ // Load documents flagged as always_load
166
+ const alwaysLoadDocs = [];
167
+ const documents = index.documents || index.files || [];
168
+ for (const doc of documents) {
169
+ if (!doc.always_load) continue;
170
+ const docPath = doc.path || doc.file;
171
+ if (!docPath) continue;
172
+ const content = await safeRead(readFile, join(projectPath, 'knowledge', docPath));
173
+ if (content) {
174
+ alwaysLoadDocs.push({ path: docPath, title: doc.title || docPath, content });
175
+ }
176
+ }
177
+
178
+ return { index, alwaysLoadDocs };
179
+ }
180
+
181
+ /**
182
+ * Parse YAML frontmatter from a markdown file.
183
+ * Frontmatter is delimited by --- at the start of the file.
184
+ * @returns {{ frontmatter: object|null, body: string }}
185
+ */
186
+ function parseFrontmatter(content) {
187
+ if (!content.startsWith('---')) {
188
+ return { frontmatter: null, body: content.trim() };
189
+ }
190
+ const endIdx = content.indexOf('---', 3);
191
+ if (endIdx === -1) {
192
+ return { frontmatter: null, body: content.trim() };
193
+ }
194
+ const fmRaw = content.slice(3, endIdx).trim();
195
+ const body = content.slice(endIdx + 3).trim();
196
+ try {
197
+ const frontmatter = yaml.load(fmRaw);
198
+ return { frontmatter: frontmatter && typeof frontmatter === 'object' ? frontmatter : null, body };
199
+ } catch {
200
+ return { frontmatter: null, body: content.trim() };
201
+ }
202
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Gitagent Prompt Builder
3
+ *
4
+ * Converts a GitagentDefinition (from parser.js) into strings/configs
5
+ * that can be injected into Claude SDK options.
6
+ */
7
+
8
+ /**
9
+ * Model name mapping from gitagent spec model IDs to UpfynAI model IDs.
10
+ * Gitagent uses full Anthropic model IDs; we map to short names.
11
+ */
12
+ const MODEL_MAP = {
13
+ 'claude-opus-4-6': 'opus',
14
+ 'claude-opus-4-20250514': 'opus',
15
+ 'claude-sonnet-4-6': 'sonnet',
16
+ 'claude-sonnet-4-5-20250929': 'sonnet',
17
+ 'claude-haiku-4-5-20251001': 'haiku',
18
+ // Generic short names pass through
19
+ 'opus': 'opus',
20
+ 'sonnet': 'sonnet',
21
+ 'haiku': 'haiku',
22
+ };
23
+
24
+ /**
25
+ * Build a system prompt appendix from a parsed gitagent definition.
26
+ * Wrapped in <gitagent-context> tags so Claude can distinguish it.
27
+ *
28
+ * @param {import('./parser.js').GitagentDefinition} def
29
+ * @returns {string}
30
+ */
31
+ export function buildSystemPromptAppendix(def) {
32
+ const sections = [];
33
+
34
+ // Identity header from manifest
35
+ const m = def.manifest;
36
+ sections.push(`# Gitagent: ${m.name} v${m.version || '0.0.0'}`);
37
+ if (m.description) sections.push(m.description);
38
+
39
+ // SOUL — agent identity & personality
40
+ if (def.soul) {
41
+ sections.push(`## Identity (SOUL)\n${def.soul}`);
42
+ }
43
+
44
+ // RULES — hard constraints
45
+ if (def.rules) {
46
+ sections.push(`## Rules\n${def.rules}`);
47
+ }
48
+
49
+ // AGENTS — framework-agnostic instructions
50
+ if (def.agents) {
51
+ sections.push(`## Agent Instructions\n${def.agents}`);
52
+ }
53
+
54
+ // Skills
55
+ if (def.skills && def.skills.length > 0) {
56
+ const skillLines = def.skills.map(s => {
57
+ const header = `### ${s.name}`;
58
+ const meta = [];
59
+ if (s.version) meta.push(`v${s.version}`);
60
+ if (s.tools && s.tools.length) meta.push(`tools: ${s.tools.join(', ')}`);
61
+ const metaLine = meta.length ? ` (${meta.join(' | ')})` : '';
62
+ return `${header}${metaLine}\n${s.description || ''}`;
63
+ });
64
+ sections.push(`## Skills\n${skillLines.join('\n\n')}`);
65
+ }
66
+
67
+ // Tools
68
+ if (def.tools && def.tools.length > 0) {
69
+ const toolLines = def.tools.map(t => {
70
+ const parts = [`### ${t.name}`];
71
+ if (t.description) parts.push(t.description);
72
+ if (t.input_schema || t.parameters) {
73
+ const schema = t.input_schema || t.parameters;
74
+ if (schema.properties) {
75
+ const props = Object.entries(schema.properties)
76
+ .map(([k, v]) => `- \`${k}\`: ${v.type || 'any'}${v.description ? ' — ' + v.description : ''}`)
77
+ .join('\n');
78
+ parts.push(`Parameters:\n${props}`);
79
+ }
80
+ }
81
+ return parts.join('\n');
82
+ });
83
+ sections.push(`## Tools\n${toolLines.join('\n\n')}`);
84
+ }
85
+
86
+ // Knowledge (always-loaded docs)
87
+ if (def.knowledge && def.knowledge.alwaysLoadDocs && def.knowledge.alwaysLoadDocs.length > 0) {
88
+ const knowledgeLines = def.knowledge.alwaysLoadDocs.map(d =>
89
+ `### ${d.title}\n${d.content}`
90
+ );
91
+ sections.push(`## Knowledge\n${knowledgeLines.join('\n\n')}`);
92
+ }
93
+
94
+ // Memory
95
+ if (def.memory) {
96
+ sections.push(`## Memory\n${def.memory}`);
97
+ }
98
+
99
+ // Runtime constraints
100
+ if (m.runtime) {
101
+ const rc = [];
102
+ if (m.runtime.max_turns) rc.push(`Max turns: ${m.runtime.max_turns}`);
103
+ if (m.runtime.timeout) rc.push(`Timeout: ${m.runtime.timeout}s`);
104
+ if (rc.length) sections.push(`## Runtime\n${rc.join('\n')}`);
105
+ }
106
+
107
+ return sections.join('\n\n');
108
+ }
109
+
110
+ /**
111
+ * Map a gitagent model config to an UpfynAI model ID.
112
+ * @param {Object|string} modelConfig - model field from agent.yaml
113
+ * @returns {string|null} UpfynAI model ID or null if no mapping
114
+ */
115
+ export function mapGitagentModel(modelConfig) {
116
+ if (!modelConfig) return null;
117
+
118
+ // String shorthand: model: "claude-sonnet-4-5-20250929"
119
+ if (typeof modelConfig === 'string') {
120
+ return MODEL_MAP[modelConfig] || null;
121
+ }
122
+
123
+ // Object form: model: { preferred: "...", fallback: [...] }
124
+ if (modelConfig.preferred) {
125
+ return MODEL_MAP[modelConfig.preferred] || null;
126
+ }
127
+
128
+ return null;
129
+ }
130
+
131
+ /**
132
+ * Extract a deduplicated list of allowed tool names from all parsed skills.
133
+ * @param {Array} skills - Parsed skill definitions
134
+ * @returns {string[]}
135
+ */
136
+ export function extractAllowedTools(skills) {
137
+ if (!skills || skills.length === 0) return [];
138
+
139
+ const toolSet = new Set();
140
+ for (const skill of skills) {
141
+ if (skill.tools && Array.isArray(skill.tools)) {
142
+ for (const t of skill.tools) {
143
+ toolSet.add(t);
144
+ }
145
+ }
146
+ if (skill.allowed_tools && Array.isArray(skill.allowed_tools)) {
147
+ for (const t of skill.allowed_tools) {
148
+ toolSet.add(t);
149
+ }
150
+ }
151
+ }
152
+ return Array.from(toolSet);
153
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "upfynai-code",
3
- "version": "2.8.3",
3
+ "version": "2.8.4",
4
4
  "description": "Unified AI coding interface — access AI chat, terminal, file explorer, git, and visual canvas from any browser. Connect your local machine and code from anywhere.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -62,6 +62,7 @@
62
62
  "commander": "^12.1.0",
63
63
  "express": "^4.21.0",
64
64
  "http-proxy-middleware": "^3.0.0",
65
+ "js-yaml": "^4.1.0",
65
66
  "open": "^10.1.0",
66
67
  "prompts": "^2.4.2",
67
68
  "ws": "^8.18.0"
@@ -24,6 +24,7 @@ const files = [
24
24
  'index.js', 'utils.js',
25
25
  'claude.js', 'codex.js', 'cursor.js',
26
26
  'shell.js', 'files.js', 'git.js', 'exec.js', 'detect.js',
27
+ 'gitagent.js',
27
28
  ];
28
29
 
29
30
  let copied = 0;
@@ -38,4 +39,20 @@ for (const file of files) {
38
39
  }
39
40
  }
40
41
 
41
- console.log(` [prepublish] Copied ${copied} agent files to dist/agents/`);
42
+ // Copy gitagent module (parser + prompt-builder) needed by gitagent agent
43
+ const sharedGitagent = join(cliRoot, '..', 'shared', 'gitagent');
44
+ const distGitagent = join(cliRoot, 'dist', 'gitagent');
45
+ if (existsSync(distGitagent)) rmSync(distGitagent, { recursive: true });
46
+ mkdirSync(distGitagent, { recursive: true });
47
+
48
+ const gitagentFiles = ['index.js', 'parser.js', 'prompt-builder.js'];
49
+ for (const file of gitagentFiles) {
50
+ const src = join(sharedGitagent, file);
51
+ const dest = join(distGitagent, file);
52
+ if (existsSync(src)) {
53
+ cpSync(src, dest);
54
+ copied++;
55
+ }
56
+ }
57
+
58
+ console.log(` [prepublish] Copied ${copied} files to dist/`);