newo 1.2.2 → 1.4.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.
- package/.env.example +2 -2
- package/CHANGELOG.md +135 -0
- package/README.md +59 -11
- package/package.json +17 -4
- package/src/akb.js +112 -0
- package/src/api.js +10 -0
- package/src/cli.js +63 -9
- package/src/fsutil.js +11 -3
- package/src/sync.js +166 -101
package/.env.example
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# NEWO endpoints
|
|
2
2
|
NEWO_BASE_URL=https://app.newo.ai
|
|
3
3
|
|
|
4
|
-
# Project you want to sync
|
|
5
|
-
NEWO_PROJECT_ID=b78188ba-0df0-46a8-8713-f0d7cff0a06e
|
|
4
|
+
# Project you want to sync (optional - leave blank to sync all accessible projects)
|
|
5
|
+
# NEWO_PROJECT_ID=b78188ba-0df0-46a8-8713-f0d7cff0a06e
|
|
6
6
|
|
|
7
7
|
# Auth (choose one)
|
|
8
8
|
# 1) Recommended: API key that can be exchanged for tokens:
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.4.0] - 2025-08-20
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Multi-Project Support**: Major feature allowing users to work with multiple NEWO projects
|
|
12
|
+
- Optional `NEWO_PROJECT_ID` environment variable - if not set, pulls all accessible projects
|
|
13
|
+
- New API endpoint: `GET /api/v1/designer/projects` to list all accessible projects
|
|
14
|
+
- Projects stored in organized folder structure: `./projects/{project-idn}/`
|
|
15
|
+
- Each project folder contains `metadata.json` with complete project information
|
|
16
|
+
- Project-specific `flows.yaml` files for individual project structure exports
|
|
17
|
+
- **Enhanced Project Structure**:
|
|
18
|
+
- Changed from single `./project/` to multi-project `./projects/{project-idn}/` hierarchy
|
|
19
|
+
- Backward compatibility maintained for existing single-project workflows
|
|
20
|
+
- Improved organization with project-specific metadata and flows
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- **Folder Structure**: Project files now stored in `./projects/{project-idn}/` instead of `./project/`
|
|
24
|
+
- **CLI Behavior**: `newo pull` now downloads all projects by default (unless NEWO_PROJECT_ID specified)
|
|
25
|
+
- **CI/CD Paths**: GitHub Actions workflow paths updated from `project/**/*` to `projects/**/*`
|
|
26
|
+
- **Help Documentation**: Updated CLI help text to reflect multi-project capabilities
|
|
27
|
+
- **API Integration**: Enhanced sync logic to handle both single and multi-project scenarios
|
|
28
|
+
|
|
29
|
+
### Technical Details
|
|
30
|
+
- **New API Functions**:
|
|
31
|
+
- `listProjects()`: Fetch all accessible projects from NEWO platform
|
|
32
|
+
- `pullSingleProject()`: Pull individual project with metadata generation
|
|
33
|
+
- `metadataPath()`: Generate project-specific metadata file paths
|
|
34
|
+
- **Enhanced Sync Engine**:
|
|
35
|
+
- Multi-project mapping in `.newo/map.json` with backward compatibility
|
|
36
|
+
- Project-specific hash tracking for efficient change detection
|
|
37
|
+
- Automatic project metadata collection and storage
|
|
38
|
+
- **File System Updates**:
|
|
39
|
+
- Updated `fsutil.js` with multi-project path utilities
|
|
40
|
+
- Enhanced `skillPath()` function to include project identifier
|
|
41
|
+
- New `projectDir()` and `metadataPath()` helper functions
|
|
42
|
+
|
|
43
|
+
### Migration Guide
|
|
44
|
+
- **Existing Users**: Single-project setups continue to work with `NEWO_PROJECT_ID` set
|
|
45
|
+
- **New Users**: Leave `NEWO_PROJECT_ID` unset to access all projects automatically
|
|
46
|
+
- **File Paths**: Update any scripts referencing `./project/` to use `./projects/{project-idn}/`
|
|
47
|
+
- **CI/CD**: Update workflow paths from `project/**/*` to `projects/**/*`
|
|
48
|
+
|
|
49
|
+
### Example Usage
|
|
50
|
+
```bash
|
|
51
|
+
# Pull all accessible projects (new default behavior)
|
|
52
|
+
npx newo pull
|
|
53
|
+
|
|
54
|
+
# Pull specific project (original behavior)
|
|
55
|
+
NEWO_PROJECT_ID=your-project-id npx newo pull
|
|
56
|
+
|
|
57
|
+
# Push changes from any project structure
|
|
58
|
+
npx newo push
|
|
59
|
+
|
|
60
|
+
# Status works with both single and multi-project setups
|
|
61
|
+
npx newo status
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## [1.3.0] - 2025-08-20
|
|
65
|
+
|
|
66
|
+
### Added
|
|
67
|
+
- **AKB Import Feature**: New `import-akb` command to import knowledge base articles from structured text files
|
|
68
|
+
- Parse multi-article files with standardized format (separated by `---`)
|
|
69
|
+
- Extract article metadata: ID, category, summary, keywords, and pricing data
|
|
70
|
+
- Import articles to NEWO personas via `/api/v1/akb/append-manual` endpoint
|
|
71
|
+
- Support for verbose logging with `--verbose` flag
|
|
72
|
+
- Progress tracking with success/failure counts
|
|
73
|
+
- **Enhanced CLI**: Added `import-akb <file> <persona_id>` command with validation and error handling
|
|
74
|
+
- **New API Endpoint**: `importAkbArticle()` function for AKB article imports
|
|
75
|
+
- **Documentation**: Comprehensive AKB format documentation in README.md
|
|
76
|
+
|
|
77
|
+
### Changed
|
|
78
|
+
- Updated help text to include new `import-akb` command
|
|
79
|
+
- Enhanced CLI command parsing to handle AKB import arguments
|
|
80
|
+
- Updated project documentation with AKB import workflow and examples
|
|
81
|
+
|
|
82
|
+
### Technical Details
|
|
83
|
+
- **New Files**:
|
|
84
|
+
- `src/akb.js`: AKB file parser and article formatter
|
|
85
|
+
- Article parsing supports category/subcategory structure with pricing data
|
|
86
|
+
- **API Integration**:
|
|
87
|
+
- Articles mapped with `topic_name` (descriptive title) and `source` (article ID)
|
|
88
|
+
- Full category content stored in `topic_summary`
|
|
89
|
+
- Structured metadata in `topic_facts` array
|
|
90
|
+
- **Error Handling**: Comprehensive validation for file paths, persona IDs, and API responses
|
|
91
|
+
|
|
92
|
+
### Example Usage
|
|
93
|
+
```bash
|
|
94
|
+
# Import AKB articles from file to specific persona
|
|
95
|
+
npx newo import-akb akb.txt da4550db-2b95-4500-91ff-fb4b60fe7be9
|
|
96
|
+
|
|
97
|
+
# With verbose logging
|
|
98
|
+
npx newo import-akb akb.txt da4550db-2b95-4500-91ff-fb4b60fe7be9 --verbose
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### AKB File Format
|
|
102
|
+
```
|
|
103
|
+
---
|
|
104
|
+
# r001
|
|
105
|
+
## Category / Subcategory / Description
|
|
106
|
+
## Summary description of the category
|
|
107
|
+
## Keywords; separated; by; semicolons
|
|
108
|
+
|
|
109
|
+
<Category type="Category Name">
|
|
110
|
+
Item Name: $Price [Modifiers: modifier1, modifier2]
|
|
111
|
+
Another Item: $Price [Modifiers: modifier3]
|
|
112
|
+
</Category>
|
|
113
|
+
---
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## [1.2.2] - 2025-08-12
|
|
117
|
+
|
|
118
|
+
### Changed
|
|
119
|
+
- Updated README with API key image
|
|
120
|
+
- Removed unused files and .DS_Store entries
|
|
121
|
+
- Package version bump
|
|
122
|
+
|
|
123
|
+
### Fixed
|
|
124
|
+
- Repository cleanup and organization
|
|
125
|
+
|
|
126
|
+
## [1.2.1] - Previous Release
|
|
127
|
+
|
|
128
|
+
### Added
|
|
129
|
+
- Initial NEWO CLI functionality
|
|
130
|
+
- Two-way sync between NEWO platform and local files
|
|
131
|
+
- Support for .guidance and .jinja file types
|
|
132
|
+
- SHA256-based change detection
|
|
133
|
+
- Project structure export to flows.yaml
|
|
134
|
+
- GitHub Actions CI/CD integration
|
|
135
|
+
- Robust authentication with token refresh
|
package/README.md
CHANGED
|
@@ -44,6 +44,8 @@ npm install
|
|
|
44
44
|
4. **Create** a new **Connector** for this Integration
|
|
45
45
|
5. **Copy** your API key (it will look like: `458663bd41f2d1...`)
|
|
46
46
|
|
|
47
|
+

|
|
48
|
+
|
|
47
49
|
### Step 2: Setup Environment
|
|
48
50
|
```bash
|
|
49
51
|
cp .env.example .env
|
|
@@ -52,32 +54,42 @@ cp .env.example .env
|
|
|
52
54
|
|
|
53
55
|
Required environment variables:
|
|
54
56
|
- `NEWO_BASE_URL` (default `https://app.newo.ai`)
|
|
55
|
-
- `NEWO_PROJECT_ID` (your project UUID from NEWO)
|
|
56
57
|
- `NEWO_API_KEY` (your API key from Step 1)
|
|
57
58
|
|
|
58
|
-
Optional
|
|
59
|
+
Optional environment variables:
|
|
60
|
+
- `NEWO_PROJECT_ID` (specific project UUID - if not set, pulls all accessible projects)
|
|
59
61
|
- `NEWO_ACCESS_TOKEN` (direct access token)
|
|
60
62
|
- `NEWO_REFRESH_TOKEN` (refresh token)
|
|
61
63
|
- `NEWO_REFRESH_URL` (custom refresh endpoint)
|
|
62
64
|
|
|
63
65
|
## Commands
|
|
64
66
|
```bash
|
|
65
|
-
npx newo pull
|
|
66
|
-
npx newo status
|
|
67
|
-
npx newo push
|
|
67
|
+
npx newo pull # download all projects -> ./projects/ OR specific project if NEWO_PROJECT_ID set
|
|
68
|
+
npx newo status # list modified files
|
|
69
|
+
npx newo push # upload modified *.guidance/*.jinja back to NEWO
|
|
70
|
+
npx newo import-akb <file> <persona_id> # import AKB articles from file
|
|
71
|
+
npx newo meta # get project metadata (debug, requires NEWO_PROJECT_ID)
|
|
68
72
|
```
|
|
69
73
|
|
|
74
|
+
### Project Structure
|
|
70
75
|
Files are stored as:
|
|
71
|
-
- `./
|
|
72
|
-
- `./
|
|
76
|
+
- **Multi-project mode** (no NEWO_PROJECT_ID): `./projects/<ProjectIdn>/<AgentIdn>/<FlowIdn>/<SkillIdn>.guidance|.jinja`
|
|
77
|
+
- **Single-project mode** (NEWO_PROJECT_ID set): `./projects/<ProjectIdn>/<AgentIdn>/<FlowIdn>/<SkillIdn>.guidance|.jinja`
|
|
78
|
+
|
|
79
|
+
Each project folder contains:
|
|
80
|
+
- `metadata.json` - Project metadata (title, description, version, etc.)
|
|
81
|
+
- `flows.yaml` - Complete project structure export for external tools
|
|
82
|
+
- Agent/Flow/Skill hierarchy with `.guidance` (AI prompts) and `.jinja` (NSL templates)
|
|
73
83
|
|
|
74
84
|
Hashes are tracked in `.newo/hashes.json` so only changed files are pushed.
|
|
75
|
-
Project structure is also exported to `flows.yaml` for reference.
|
|
76
85
|
|
|
77
86
|
## Features
|
|
87
|
+
- **Multi-project support**: Pull all accessible projects or specify a single project
|
|
78
88
|
- **Two-way sync**: Pull NEWO projects to local files, push local changes back
|
|
79
89
|
- **Change detection**: SHA256 hashing prevents unnecessary uploads
|
|
80
90
|
- **Multiple file types**: `.guidance` (AI prompts) and `.jinja` (NSL templates)
|
|
91
|
+
- **Project metadata**: Each project includes `metadata.json` with complete project info
|
|
92
|
+
- **AKB import**: Import knowledge base articles from structured text files
|
|
81
93
|
- **Project structure export**: Generates `flows.yaml` with complete project metadata
|
|
82
94
|
- **Robust authentication**: API key exchange with automatic token refresh
|
|
83
95
|
- **CI/CD ready**: GitHub Actions workflow included
|
|
@@ -90,8 +102,8 @@ on:
|
|
|
90
102
|
push:
|
|
91
103
|
branches: [ main ]
|
|
92
104
|
paths:
|
|
93
|
-
- '
|
|
94
|
-
- '
|
|
105
|
+
- 'projects/**/*.guidance'
|
|
106
|
+
- 'projects/**/*.jinja'
|
|
95
107
|
jobs:
|
|
96
108
|
deploy:
|
|
97
109
|
runs-on: ubuntu-latest
|
|
@@ -110,11 +122,47 @@ jobs:
|
|
|
110
122
|
# NEWO_REFRESH_URL: ${{ secrets.NEWO_REFRESH_URL }}
|
|
111
123
|
```
|
|
112
124
|
|
|
125
|
+
## AKB Import
|
|
126
|
+
|
|
127
|
+
Import knowledge base articles from structured text files into NEWO personas:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
npx newo import-akb akb.txt da4550db-2b95-4500-91ff-fb4b60fe7be9
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### AKB File Format
|
|
134
|
+
```
|
|
135
|
+
---
|
|
136
|
+
# r001
|
|
137
|
+
## Category / Subcategory / Description
|
|
138
|
+
## Summary description of the category
|
|
139
|
+
## Keywords; separated; by; semicolons
|
|
140
|
+
|
|
141
|
+
<Category type="Category Name">
|
|
142
|
+
Item Name: $Price [Modifiers: modifier1, modifier2]
|
|
143
|
+
Another Item: $Price [Modifiers: modifier3]
|
|
144
|
+
</Category>
|
|
145
|
+
---
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Each article will be imported with:
|
|
149
|
+
- **topic_name**: The descriptive category title
|
|
150
|
+
- **source**: The article ID (e.g., "r001")
|
|
151
|
+
- **topic_summary**: The full category content with pricing
|
|
152
|
+
- **topic_facts**: Array containing category, summary, and keywords
|
|
153
|
+
- **confidence**: 100
|
|
154
|
+
- **labels**: ["rag_context"]
|
|
155
|
+
|
|
156
|
+
Use `--verbose` flag to see detailed import progress.
|
|
157
|
+
|
|
113
158
|
## API Endpoints
|
|
159
|
+
- `GET /api/v1/designer/projects` - List all accessible projects
|
|
160
|
+
- `GET /api/v1/designer/projects/by-id/{projectId}` - Get specific project metadata
|
|
114
161
|
- `GET /api/v1/bff/agents/list?project_id=...` - List project agents
|
|
115
162
|
- `GET /api/v1/designer/flows/{flowId}/skills` - List skills in flow
|
|
116
163
|
- `GET /api/v1/designer/skills/{skillId}` - Get skill content
|
|
117
164
|
- `PUT /api/v1/designer/flows/skills/{skillId}` - Update skill content
|
|
118
165
|
- `GET /api/v1/designer/flows/{flowId}/events` - List flow events (for flows.yaml)
|
|
119
166
|
- `GET /api/v1/designer/flows/{flowId}/states` - List flow states (for flows.yaml)
|
|
120
|
-
- `POST /api/v1/auth/api-key/token` - Exchange API key for access tokens
|
|
167
|
+
- `POST /api/v1/auth/api-key/token` - Exchange API key for access tokens
|
|
168
|
+
- `POST /api/v1/akb/append-manual` - Import AKB articles
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "newo",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "NEWO CLI: sync flows/skills between NEWO and local files",
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "NEWO CLI: sync flows/skills between NEWO and local files, multi-project support, import AKB articles",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"newo": "src/cli.js"
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"src/**/*.js",
|
|
11
11
|
"README.md",
|
|
12
|
+
"CHANGELOG.md",
|
|
12
13
|
".env.example"
|
|
13
14
|
],
|
|
14
15
|
"keywords": [
|
|
@@ -18,7 +19,12 @@
|
|
|
18
19
|
"agent",
|
|
19
20
|
"automation",
|
|
20
21
|
"sync",
|
|
21
|
-
"local-development"
|
|
22
|
+
"local-development",
|
|
23
|
+
"akb",
|
|
24
|
+
"knowledge-base",
|
|
25
|
+
"import",
|
|
26
|
+
"multi-project",
|
|
27
|
+
"workspace"
|
|
22
28
|
],
|
|
23
29
|
"author": "sabbah13",
|
|
24
30
|
"license": "MIT",
|
|
@@ -40,10 +46,17 @@
|
|
|
40
46
|
"js-yaml": "^4.1.0",
|
|
41
47
|
"minimist": "^1.2.8"
|
|
42
48
|
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"mocha": "^10.2.0"
|
|
51
|
+
},
|
|
43
52
|
"scripts": {
|
|
44
53
|
"dev": "node ./src/cli.js",
|
|
45
54
|
"pull": "node ./src/cli.js pull",
|
|
46
55
|
"push": "node ./src/cli.js push",
|
|
47
|
-
"status": "node ./src/cli.js status"
|
|
56
|
+
"status": "node ./src/cli.js status",
|
|
57
|
+
"test": "mocha test/*.test.js --timeout 60000",
|
|
58
|
+
"test:api": "mocha test/api.test.js --timeout 30000",
|
|
59
|
+
"test:sync": "mocha test/sync.test.js --timeout 60000",
|
|
60
|
+
"test:integration": "mocha test/integration.test.js --timeout 120000"
|
|
48
61
|
}
|
|
49
62
|
}
|
package/src/akb.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse AKB file and extract articles
|
|
5
|
+
* @param {string} filePath - Path to AKB file
|
|
6
|
+
* @returns {Array} Array of parsed articles
|
|
7
|
+
*/
|
|
8
|
+
function parseAkbFile(filePath) {
|
|
9
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
10
|
+
const articles = [];
|
|
11
|
+
|
|
12
|
+
// Split by article separators (---)
|
|
13
|
+
const sections = content.split(/^---\s*$/gm).filter(section => section.trim());
|
|
14
|
+
|
|
15
|
+
for (const section of sections) {
|
|
16
|
+
const lines = section.split('\n').filter(line => line.trim());
|
|
17
|
+
if (lines.length === 0) continue;
|
|
18
|
+
|
|
19
|
+
const article = parseArticleSection(lines);
|
|
20
|
+
if (article) {
|
|
21
|
+
articles.push(article);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return articles;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Parse individual article section
|
|
30
|
+
* @param {Array} lines - Lines of the article section
|
|
31
|
+
* @returns {Object|null} Parsed article object
|
|
32
|
+
*/
|
|
33
|
+
function parseArticleSection(lines) {
|
|
34
|
+
let topicName = '';
|
|
35
|
+
let category = '';
|
|
36
|
+
let summary = '';
|
|
37
|
+
let keywords = '';
|
|
38
|
+
let topicSummary = '';
|
|
39
|
+
|
|
40
|
+
// Find topic name (# r001)
|
|
41
|
+
const topicLine = lines.find(line => line.match(/^#\s+r\d+/));
|
|
42
|
+
if (!topicLine) return null;
|
|
43
|
+
|
|
44
|
+
topicName = topicLine.replace(/^#\s+/, '').trim();
|
|
45
|
+
|
|
46
|
+
// Extract category/subcategory/description (first ## line)
|
|
47
|
+
const categoryLine = lines.find(line => line.startsWith('## ') && line.includes(' / '));
|
|
48
|
+
if (categoryLine) {
|
|
49
|
+
category = categoryLine.replace(/^##\s+/, '').trim();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Extract summary (second ## line)
|
|
53
|
+
const summaryLineIndex = lines.findIndex(line => line.startsWith('## ') && line.includes(' / '));
|
|
54
|
+
if (summaryLineIndex >= 0 && summaryLineIndex + 1 < lines.length) {
|
|
55
|
+
const nextLine = lines[summaryLineIndex + 1];
|
|
56
|
+
if (nextLine.startsWith('## ') && !nextLine.includes(' / ')) {
|
|
57
|
+
summary = nextLine.replace(/^##\s+/, '').trim();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Extract keywords (third ## line)
|
|
62
|
+
const keywordsLineIndex = lines.findIndex((line, index) =>
|
|
63
|
+
index > summaryLineIndex + 1 && line.startsWith('## ') && !line.includes(' / ')
|
|
64
|
+
);
|
|
65
|
+
if (keywordsLineIndex >= 0) {
|
|
66
|
+
keywords = lines[keywordsLineIndex].replace(/^##\s+/, '').trim();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Extract category content
|
|
70
|
+
const categoryStartIndex = lines.findIndex(line => line.includes('<Category type='));
|
|
71
|
+
const categoryEndIndex = lines.findIndex(line => line.includes('</Category>'));
|
|
72
|
+
|
|
73
|
+
if (categoryStartIndex >= 0 && categoryEndIndex >= 0) {
|
|
74
|
+
const categoryLines = lines.slice(categoryStartIndex, categoryEndIndex + 1);
|
|
75
|
+
topicSummary = categoryLines.join('\n');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Create topic_facts array
|
|
79
|
+
const topicFacts = [
|
|
80
|
+
category,
|
|
81
|
+
summary,
|
|
82
|
+
keywords
|
|
83
|
+
].filter(fact => fact.trim() !== '');
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
topic_name: category, // Use the descriptive title as topic_name
|
|
87
|
+
persona_id: null, // Will be set when importing
|
|
88
|
+
topic_summary: topicSummary,
|
|
89
|
+
topic_facts: topicFacts,
|
|
90
|
+
confidence: 100,
|
|
91
|
+
source: topicName, // Use the ID (r001) as source
|
|
92
|
+
labels: ["rag_context"]
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Convert parsed articles to API format for bulk import
|
|
98
|
+
* @param {Array} articles - Parsed articles
|
|
99
|
+
* @param {string} personaId - Target persona ID
|
|
100
|
+
* @returns {Array} Array of articles ready for API import
|
|
101
|
+
*/
|
|
102
|
+
function prepareArticlesForImport(articles, personaId) {
|
|
103
|
+
return articles.map(article => ({
|
|
104
|
+
...article,
|
|
105
|
+
persona_id: personaId
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export {
|
|
110
|
+
parseAkbFile,
|
|
111
|
+
prepareArticlesForImport
|
|
112
|
+
};
|
package/src/api.js
CHANGED
|
@@ -58,6 +58,11 @@ export async function makeClient(verbose = false) {
|
|
|
58
58
|
return client;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
export async function listProjects(client) {
|
|
62
|
+
const r = await client.get(`/api/v1/designer/projects`);
|
|
63
|
+
return r.data;
|
|
64
|
+
}
|
|
65
|
+
|
|
61
66
|
export async function listAgents(client, projectId) {
|
|
62
67
|
const r = await client.get(`/api/v1/bff/agents/list`, { params: { project_id: projectId } });
|
|
63
68
|
return r.data;
|
|
@@ -90,4 +95,9 @@ export async function listFlowEvents(client, flowId) {
|
|
|
90
95
|
export async function listFlowStates(client, flowId) {
|
|
91
96
|
const r = await client.get(`/api/v1/designer/flows/${flowId}/states`);
|
|
92
97
|
return r.data;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function importAkbArticle(client, articleData) {
|
|
101
|
+
const r = await client.post('/api/v1/akb/append-manual', articleData);
|
|
102
|
+
return r.data;
|
|
93
103
|
}
|
package/src/cli.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import minimist from 'minimist';
|
|
3
3
|
import dotenv from 'dotenv';
|
|
4
|
-
import { makeClient, getProjectMeta } from './api.js';
|
|
4
|
+
import { makeClient, getProjectMeta, importAkbArticle } from './api.js';
|
|
5
5
|
import { pullAll, pushChanged, status } from './sync.js';
|
|
6
|
+
import { parseAkbFile, prepareArticlesForImport } from './akb.js';
|
|
7
|
+
import path from 'path';
|
|
6
8
|
|
|
7
9
|
dotenv.config();
|
|
8
10
|
const { NEWO_PROJECT_ID } = process.env;
|
|
@@ -15,16 +17,24 @@ async function main() {
|
|
|
15
17
|
if (!cmd || ['help', '-h', '--help'].includes(cmd)) {
|
|
16
18
|
console.log(`NEWO CLI
|
|
17
19
|
Usage:
|
|
18
|
-
newo pull
|
|
19
|
-
newo push
|
|
20
|
-
newo status
|
|
21
|
-
newo meta
|
|
20
|
+
newo pull # download all projects -> ./projects/ OR specific project if NEWO_PROJECT_ID set
|
|
21
|
+
newo push # upload modified *.guidance/*.jinja back to NEWO
|
|
22
|
+
newo status # show modified files
|
|
23
|
+
newo meta # get project metadata (debug, requires NEWO_PROJECT_ID)
|
|
24
|
+
newo import-akb <file> <persona_id> # import AKB articles from file
|
|
22
25
|
|
|
23
26
|
Flags:
|
|
24
|
-
--verbose, -v
|
|
27
|
+
--verbose, -v # enable detailed logging
|
|
25
28
|
|
|
26
29
|
Env:
|
|
27
|
-
NEWO_BASE_URL, NEWO_PROJECT_ID, NEWO_API_KEY, NEWO_REFRESH_URL (optional)
|
|
30
|
+
NEWO_BASE_URL, NEWO_PROJECT_ID (optional), NEWO_API_KEY, NEWO_REFRESH_URL (optional)
|
|
31
|
+
|
|
32
|
+
Notes:
|
|
33
|
+
- multi-project support: pull downloads all accessible projects or single project based on NEWO_PROJECT_ID
|
|
34
|
+
- If NEWO_PROJECT_ID is set, pull downloads only that project
|
|
35
|
+
- If NEWO_PROJECT_ID is not set, pull downloads all projects accessible with your API key
|
|
36
|
+
- Projects are stored in ./projects/{project-idn}/ folders
|
|
37
|
+
- Each project folder contains metadata.json and flows.yaml
|
|
28
38
|
`);
|
|
29
39
|
return;
|
|
30
40
|
}
|
|
@@ -32,8 +42,8 @@ Env:
|
|
|
32
42
|
const client = await makeClient(verbose);
|
|
33
43
|
|
|
34
44
|
if (cmd === 'pull') {
|
|
35
|
-
|
|
36
|
-
await pullAll(client, NEWO_PROJECT_ID, verbose);
|
|
45
|
+
// If PROJECT_ID is set, pull single project; otherwise pull all projects
|
|
46
|
+
await pullAll(client, NEWO_PROJECT_ID || null, verbose);
|
|
37
47
|
} else if (cmd === 'push') {
|
|
38
48
|
await pushChanged(client, verbose);
|
|
39
49
|
} else if (cmd === 'status') {
|
|
@@ -42,6 +52,50 @@ Env:
|
|
|
42
52
|
if (!NEWO_PROJECT_ID) throw new Error('NEWO_PROJECT_ID is not set in env');
|
|
43
53
|
const meta = await getProjectMeta(client, NEWO_PROJECT_ID);
|
|
44
54
|
console.log(JSON.stringify(meta, null, 2));
|
|
55
|
+
} else if (cmd === 'import-akb') {
|
|
56
|
+
const akbFile = args._[1];
|
|
57
|
+
const personaId = args._[2];
|
|
58
|
+
|
|
59
|
+
if (!akbFile || !personaId) {
|
|
60
|
+
console.error('Usage: newo import-akb <file> <persona_id>');
|
|
61
|
+
console.error('Example: newo import-akb akb.txt da4550db-2b95-4500-91ff-fb4b60fe7be9');
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const filePath = path.resolve(akbFile);
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
if (verbose) console.log(`📖 Parsing AKB file: ${filePath}`);
|
|
69
|
+
const articles = parseAkbFile(filePath);
|
|
70
|
+
console.log(`✓ Parsed ${articles.length} articles from ${akbFile}`);
|
|
71
|
+
|
|
72
|
+
if (verbose) console.log(`🔧 Preparing articles for persona: ${personaId}`);
|
|
73
|
+
const preparedArticles = prepareArticlesForImport(articles, personaId);
|
|
74
|
+
|
|
75
|
+
let successCount = 0;
|
|
76
|
+
let errorCount = 0;
|
|
77
|
+
|
|
78
|
+
console.log(`📤 Importing ${preparedArticles.length} articles...`);
|
|
79
|
+
|
|
80
|
+
for (const [index, article] of preparedArticles.entries()) {
|
|
81
|
+
try {
|
|
82
|
+
if (verbose) console.log(` [${index + 1}/${preparedArticles.length}] Importing ${article.topic_name}...`);
|
|
83
|
+
await importAkbArticle(client, article);
|
|
84
|
+
successCount++;
|
|
85
|
+
if (!verbose) process.stdout.write('.');
|
|
86
|
+
} catch (error) {
|
|
87
|
+
errorCount++;
|
|
88
|
+
console.error(`\n❌ Failed to import ${article.topic_name}:`, error?.response?.data || error.message);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!verbose) console.log(''); // new line after dots
|
|
93
|
+
console.log(`✅ Import complete: ${successCount} successful, ${errorCount} failed`);
|
|
94
|
+
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error('❌ AKB import failed:', error.message);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
45
99
|
} else {
|
|
46
100
|
console.error('Unknown command:', cmd);
|
|
47
101
|
process.exit(1);
|
package/src/fsutil.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
|
|
4
|
-
export const ROOT_DIR = path.join(process.cwd(), '
|
|
4
|
+
export const ROOT_DIR = path.join(process.cwd(), 'projects');
|
|
5
5
|
export const STATE_DIR = path.join(process.cwd(), '.newo');
|
|
6
6
|
export const MAP_PATH = path.join(STATE_DIR, 'map.json');
|
|
7
7
|
export const HASHES_PATH = path.join(STATE_DIR, 'hashes.json');
|
|
@@ -11,9 +11,17 @@ export async function ensureState() {
|
|
|
11
11
|
await fs.ensureDir(ROOT_DIR);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export function
|
|
14
|
+
export function projectDir(projectIdn) {
|
|
15
|
+
return path.join(ROOT_DIR, projectIdn);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function skillPath(projectIdn, agentIdn, flowIdn, skillIdn, runnerType = 'guidance') {
|
|
15
19
|
const extension = runnerType === 'nsl' ? '.jinja' : '.guidance';
|
|
16
|
-
return path.join(ROOT_DIR, agentIdn, flowIdn, `${skillIdn}${extension}`);
|
|
20
|
+
return path.join(ROOT_DIR, projectIdn, agentIdn, flowIdn, `${skillIdn}${extension}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function metadataPath(projectIdn) {
|
|
24
|
+
return path.join(ROOT_DIR, projectIdn, 'metadata.json');
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
export async function writeFileAtomic(filepath, content) {
|
package/src/sync.js
CHANGED
|
@@ -1,31 +1,35 @@
|
|
|
1
|
-
import { listAgents, listFlowSkills, updateSkill, listFlowEvents, listFlowStates } from './api.js';
|
|
2
|
-
import { ensureState, skillPath, writeFileAtomic, readIfExists, MAP_PATH } from './fsutil.js';
|
|
1
|
+
import { listProjects, listAgents, listFlowSkills, updateSkill, listFlowEvents, listFlowStates, getProjectMeta } from './api.js';
|
|
2
|
+
import { ensureState, skillPath, writeFileAtomic, readIfExists, MAP_PATH, projectDir, metadataPath } from './fsutil.js';
|
|
3
3
|
import fs from 'fs-extra';
|
|
4
4
|
import { sha256, loadHashes, saveHashes } from './hash.js';
|
|
5
5
|
import yaml from 'js-yaml';
|
|
6
6
|
import path from 'path';
|
|
7
7
|
|
|
8
|
-
export async function
|
|
9
|
-
|
|
10
|
-
if (verbose) console.log(`🔍 Fetching agents for project ${projectId}...`);
|
|
8
|
+
export async function pullSingleProject(client, projectId, projectIdn, verbose = false) {
|
|
9
|
+
if (verbose) console.log(`🔍 Fetching agents for project ${projectId} (${projectIdn})...`);
|
|
11
10
|
const agents = await listAgents(client, projectId);
|
|
12
11
|
if (verbose) console.log(`📦 Found ${agents.length} agents`);
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
// Get and save project metadata
|
|
14
|
+
const projectMeta = await getProjectMeta(client, projectId);
|
|
15
|
+
await writeFileAtomic(metadataPath(projectIdn), JSON.stringify(projectMeta, null, 2));
|
|
16
|
+
if (verbose) console.log(`✓ Saved metadata for ${projectIdn}`);
|
|
17
|
+
|
|
18
|
+
const projectMap = { projectId, projectIdn, agents: {} };
|
|
15
19
|
|
|
16
20
|
for (const agent of agents) {
|
|
17
21
|
const aKey = agent.idn;
|
|
18
|
-
|
|
22
|
+
projectMap.agents[aKey] = { id: agent.id, flows: {} };
|
|
19
23
|
|
|
20
24
|
for (const flow of agent.flows ?? []) {
|
|
21
|
-
|
|
25
|
+
projectMap.agents[aKey].flows[flow.idn] = { id: flow.id, skills: {} };
|
|
22
26
|
|
|
23
27
|
const skills = await listFlowSkills(client, flow.id);
|
|
24
28
|
for (const s of skills) {
|
|
25
|
-
const file = skillPath(agent.idn, flow.idn, s.idn, s.runner_type);
|
|
29
|
+
const file = skillPath(projectIdn, agent.idn, flow.idn, s.idn, s.runner_type);
|
|
26
30
|
await writeFileAtomic(file, s.prompt_script || '');
|
|
27
31
|
// Store complete skill metadata for push operations
|
|
28
|
-
|
|
32
|
+
projectMap.agents[aKey].flows[flow.idn].skills[s.idn] = {
|
|
29
33
|
id: s.id,
|
|
30
34
|
title: s.title,
|
|
31
35
|
idn: s.idn,
|
|
@@ -39,23 +43,66 @@ export async function pullAll(client, projectId, verbose = false) {
|
|
|
39
43
|
}
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
|
|
46
|
+
// Generate flows.yaml for this project
|
|
47
|
+
if (verbose) console.log(`📄 Generating flows.yaml for ${projectIdn}...`);
|
|
48
|
+
await generateFlowsYaml(client, agents, projectIdn, verbose);
|
|
49
|
+
|
|
50
|
+
return projectMap;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function pullAll(client, projectId = null, verbose = false) {
|
|
54
|
+
await ensureState();
|
|
55
|
+
|
|
56
|
+
if (projectId) {
|
|
57
|
+
// Single project mode
|
|
58
|
+
const projectMeta = await getProjectMeta(client, projectId);
|
|
59
|
+
const projectMap = await pullSingleProject(client, projectId, projectMeta.idn, verbose);
|
|
60
|
+
|
|
61
|
+
const idMap = { projects: { [projectMeta.idn]: projectMap } };
|
|
62
|
+
await fs.writeJson(MAP_PATH, idMap, { spaces: 2 });
|
|
63
|
+
|
|
64
|
+
// Generate hash tracking for this project
|
|
65
|
+
const hashes = {};
|
|
66
|
+
for (const [agentIdn, agentObj] of Object.entries(projectMap.agents)) {
|
|
67
|
+
for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
|
|
68
|
+
for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
|
|
69
|
+
const p = skillPath(projectMeta.idn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
70
|
+
const content = await fs.readFile(p, 'utf8');
|
|
71
|
+
hashes[p] = sha256(content);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
await saveHashes(hashes);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Multi-project mode
|
|
80
|
+
if (verbose) console.log(`🔍 Fetching all projects...`);
|
|
81
|
+
const projects = await listProjects(client);
|
|
82
|
+
if (verbose) console.log(`📦 Found ${projects.length} projects`);
|
|
83
|
+
|
|
84
|
+
const idMap = { projects: {} };
|
|
85
|
+
const allHashes = {};
|
|
43
86
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
87
|
+
for (const project of projects) {
|
|
88
|
+
if (verbose) console.log(`\n📁 Processing project: ${project.idn} (${project.title})`);
|
|
89
|
+
const projectMap = await pullSingleProject(client, project.id, project.idn, verbose);
|
|
90
|
+
idMap.projects[project.idn] = projectMap;
|
|
47
91
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
92
|
+
// Collect hashes for this project
|
|
93
|
+
for (const [agentIdn, agentObj] of Object.entries(projectMap.agents)) {
|
|
94
|
+
for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
|
|
95
|
+
for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
|
|
96
|
+
const p = skillPath(project.idn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
97
|
+
const content = await fs.readFile(p, 'utf8');
|
|
98
|
+
allHashes[p] = sha256(content);
|
|
99
|
+
}
|
|
55
100
|
}
|
|
56
101
|
}
|
|
57
102
|
}
|
|
58
|
-
|
|
103
|
+
|
|
104
|
+
await fs.writeJson(MAP_PATH, idMap, { spaces: 2 });
|
|
105
|
+
await saveHashes(allHashes);
|
|
59
106
|
}
|
|
60
107
|
|
|
61
108
|
export async function pushChanged(client, verbose = false) {
|
|
@@ -74,59 +121,68 @@ export async function pushChanged(client, verbose = false) {
|
|
|
74
121
|
let pushed = 0;
|
|
75
122
|
let scanned = 0;
|
|
76
123
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const oldHash = oldHashes[p];
|
|
94
|
-
if (verbose) {
|
|
95
|
-
console.log(` 🔍 Hash comparison:`);
|
|
96
|
-
console.log(` Old: ${oldHash || 'none'}`);
|
|
97
|
-
console.log(` New: ${h}`);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (oldHash !== h) {
|
|
101
|
-
if (verbose) console.log(` 🔄 File changed, preparing to push...`);
|
|
124
|
+
// Handle both old single-project format and new multi-project format
|
|
125
|
+
const projects = idMap.projects || { '': idMap };
|
|
126
|
+
|
|
127
|
+
for (const [projectIdn, projectData] of Object.entries(projects)) {
|
|
128
|
+
if (verbose && projectIdn) console.log(`📁 Scanning project: ${projectIdn}`);
|
|
129
|
+
|
|
130
|
+
for (const [agentIdn, agentObj] of Object.entries(projectData.agents)) {
|
|
131
|
+
if (verbose) console.log(` 📁 Scanning agent: ${agentIdn}`);
|
|
132
|
+
for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
|
|
133
|
+
if (verbose) console.log(` 📁 Scanning flow: ${flowIdn}`);
|
|
134
|
+
for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
|
|
135
|
+
const p = projectIdn ?
|
|
136
|
+
skillPath(projectIdn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type) :
|
|
137
|
+
skillPath('', agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
138
|
+
scanned++;
|
|
139
|
+
if (verbose) console.log(` 📄 Checking: ${p}`);
|
|
102
140
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
prompt_script: content,
|
|
109
|
-
runner_type: skillMeta.runner_type,
|
|
110
|
-
model: skillMeta.model,
|
|
111
|
-
parameters: skillMeta.parameters,
|
|
112
|
-
path: skillMeta.path
|
|
113
|
-
};
|
|
141
|
+
const content = await readIfExists(p);
|
|
142
|
+
if (content === null) {
|
|
143
|
+
if (verbose) console.log(` ⚠️ File not found: ${p}`);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
114
146
|
|
|
147
|
+
const h = sha256(content);
|
|
148
|
+
const oldHash = oldHashes[p];
|
|
115
149
|
if (verbose) {
|
|
116
|
-
console.log(`
|
|
117
|
-
console.log(`
|
|
118
|
-
console.log(`
|
|
119
|
-
console.log(` IDN: ${skillObject.idn}`);
|
|
120
|
-
console.log(` Content length: ${content.length} chars`);
|
|
121
|
-
console.log(` Content preview: ${content.substring(0, 100).replace(/\n/g, '\\n')}...`);
|
|
150
|
+
console.log(` 🔍 Hash comparison:`);
|
|
151
|
+
console.log(` Old: ${oldHash || 'none'}`);
|
|
152
|
+
console.log(` New: ${h}`);
|
|
122
153
|
}
|
|
123
154
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
155
|
+
if (oldHash !== h) {
|
|
156
|
+
if (verbose) console.log(` 🔄 File changed, preparing to push...`);
|
|
157
|
+
|
|
158
|
+
// Create complete skill object with updated prompt_script
|
|
159
|
+
const skillObject = {
|
|
160
|
+
id: skillMeta.id,
|
|
161
|
+
title: skillMeta.title,
|
|
162
|
+
idn: skillMeta.idn,
|
|
163
|
+
prompt_script: content,
|
|
164
|
+
runner_type: skillMeta.runner_type,
|
|
165
|
+
model: skillMeta.model,
|
|
166
|
+
parameters: skillMeta.parameters,
|
|
167
|
+
path: skillMeta.path
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
if (verbose) {
|
|
171
|
+
console.log(` 📤 Pushing skill object:`);
|
|
172
|
+
console.log(` ID: ${skillObject.id}`);
|
|
173
|
+
console.log(` Title: ${skillObject.title}`);
|
|
174
|
+
console.log(` IDN: ${skillObject.idn}`);
|
|
175
|
+
console.log(` Content length: ${content.length} chars`);
|
|
176
|
+
console.log(` Content preview: ${content.substring(0, 100).replace(/\n/g, '\\n')}...`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
await updateSkill(client, skillObject);
|
|
180
|
+
console.log(`↑ Pushed ${p}`);
|
|
181
|
+
newHashes[p] = h;
|
|
182
|
+
pushed++;
|
|
183
|
+
} else if (verbose) {
|
|
184
|
+
console.log(` ✓ No changes`);
|
|
185
|
+
}
|
|
130
186
|
}
|
|
131
187
|
}
|
|
132
188
|
}
|
|
@@ -149,33 +205,42 @@ export async function status(verbose = false) {
|
|
|
149
205
|
const hashes = await loadHashes();
|
|
150
206
|
let dirty = 0;
|
|
151
207
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (verbose)
|
|
177
|
-
|
|
178
|
-
|
|
208
|
+
// Handle both old single-project format and new multi-project format
|
|
209
|
+
const projects = idMap.projects || { '': idMap };
|
|
210
|
+
|
|
211
|
+
for (const [projectIdn, projectData] of Object.entries(projects)) {
|
|
212
|
+
if (verbose && projectIdn) console.log(`📁 Checking project: ${projectIdn}`);
|
|
213
|
+
|
|
214
|
+
for (const [agentIdn, agentObj] of Object.entries(projectData.agents)) {
|
|
215
|
+
if (verbose) console.log(` 📁 Checking agent: ${agentIdn}`);
|
|
216
|
+
for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
|
|
217
|
+
if (verbose) console.log(` 📁 Checking flow: ${flowIdn}`);
|
|
218
|
+
for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
|
|
219
|
+
const p = projectIdn ?
|
|
220
|
+
skillPath(projectIdn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type) :
|
|
221
|
+
skillPath('', agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
222
|
+
const exists = await fs.pathExists(p);
|
|
223
|
+
if (!exists) {
|
|
224
|
+
console.log(`D ${p}`);
|
|
225
|
+
dirty++;
|
|
226
|
+
if (verbose) console.log(` ❌ Deleted: ${p}`);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const content = await fs.readFile(p, 'utf8');
|
|
230
|
+
const h = sha256(content);
|
|
231
|
+
const oldHash = hashes[p];
|
|
232
|
+
if (verbose) {
|
|
233
|
+
console.log(` 📄 ${p}`);
|
|
234
|
+
console.log(` Old hash: ${oldHash || 'none'}`);
|
|
235
|
+
console.log(` New hash: ${h}`);
|
|
236
|
+
}
|
|
237
|
+
if (oldHash !== h) {
|
|
238
|
+
console.log(`M ${p}`);
|
|
239
|
+
dirty++;
|
|
240
|
+
if (verbose) console.log(` 🔄 Modified: ${p}`);
|
|
241
|
+
} else if (verbose) {
|
|
242
|
+
console.log(` ✓ Unchanged: ${p}`);
|
|
243
|
+
}
|
|
179
244
|
}
|
|
180
245
|
}
|
|
181
246
|
}
|
|
@@ -183,7 +248,7 @@ export async function status(verbose = false) {
|
|
|
183
248
|
console.log(dirty ? `${dirty} changed file(s).` : 'Clean.');
|
|
184
249
|
}
|
|
185
250
|
|
|
186
|
-
async function generateFlowsYaml(client, agents, verbose = false) {
|
|
251
|
+
async function generateFlowsYaml(client, agents, projectIdn, verbose = false) {
|
|
187
252
|
const flowsData = { flows: [] };
|
|
188
253
|
|
|
189
254
|
for (const agent of agents) {
|
|
@@ -278,7 +343,7 @@ async function generateFlowsYaml(client, agents, verbose = false) {
|
|
|
278
343
|
// Post-process to fix enum formatting
|
|
279
344
|
yamlContent = yamlContent.replace(/"(!enum "[^"]+")"/g, '$1');
|
|
280
345
|
|
|
281
|
-
const yamlPath = path.join('flows.yaml');
|
|
282
|
-
await
|
|
283
|
-
console.log(`✓ Generated flows.yaml`);
|
|
346
|
+
const yamlPath = path.join(projectDir(projectIdn), 'flows.yaml');
|
|
347
|
+
await writeFileAtomic(yamlPath, yamlContent);
|
|
348
|
+
console.log(`✓ Generated flows.yaml for ${projectIdn}`);
|
|
284
349
|
}
|