newo 1.3.0 → 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 +58 -2
- package/README.md +18 -9
- package/package.json +13 -4
- package/src/api.js +5 -0
- package/src/cli.js +12 -5
- 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
CHANGED
|
@@ -5,7 +5,63 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [1.
|
|
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
|
|
9
65
|
|
|
10
66
|
### Added
|
|
11
67
|
- **AKB Import Feature**: New `import-akb` command to import knowledge base articles from structured text files
|
|
@@ -57,7 +113,7 @@ Another Item: $Price [Modifiers: modifier3]
|
|
|
57
113
|
---
|
|
58
114
|
```
|
|
59
115
|
|
|
60
|
-
## [1.2.2] - 2025-
|
|
116
|
+
## [1.2.2] - 2025-08-12
|
|
61
117
|
|
|
62
118
|
### Changed
|
|
63
119
|
- Updated README with API key image
|
package/README.md
CHANGED
|
@@ -54,34 +54,41 @@ cp .env.example .env
|
|
|
54
54
|
|
|
55
55
|
Required environment variables:
|
|
56
56
|
- `NEWO_BASE_URL` (default `https://app.newo.ai`)
|
|
57
|
-
- `NEWO_PROJECT_ID` (your project UUID from NEWO)
|
|
58
57
|
- `NEWO_API_KEY` (your API key from Step 1)
|
|
59
58
|
|
|
60
|
-
Optional
|
|
59
|
+
Optional environment variables:
|
|
60
|
+
- `NEWO_PROJECT_ID` (specific project UUID - if not set, pulls all accessible projects)
|
|
61
61
|
- `NEWO_ACCESS_TOKEN` (direct access token)
|
|
62
62
|
- `NEWO_REFRESH_TOKEN` (refresh token)
|
|
63
63
|
- `NEWO_REFRESH_URL` (custom refresh endpoint)
|
|
64
64
|
|
|
65
65
|
## Commands
|
|
66
66
|
```bash
|
|
67
|
-
npx newo pull # download
|
|
67
|
+
npx newo pull # download all projects -> ./projects/ OR specific project if NEWO_PROJECT_ID set
|
|
68
68
|
npx newo status # list modified files
|
|
69
69
|
npx newo push # upload modified *.guidance/*.jinja back to NEWO
|
|
70
70
|
npx newo import-akb <file> <persona_id> # import AKB articles from file
|
|
71
|
-
npx newo meta # get project metadata (debug)
|
|
71
|
+
npx newo meta # get project metadata (debug, requires NEWO_PROJECT_ID)
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
+
### Project Structure
|
|
74
75
|
Files are stored as:
|
|
75
|
-
- `./
|
|
76
|
-
- `./
|
|
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)
|
|
77
83
|
|
|
78
84
|
Hashes are tracked in `.newo/hashes.json` so only changed files are pushed.
|
|
79
|
-
Project structure is also exported to `flows.yaml` for reference.
|
|
80
85
|
|
|
81
86
|
## Features
|
|
87
|
+
- **Multi-project support**: Pull all accessible projects or specify a single project
|
|
82
88
|
- **Two-way sync**: Pull NEWO projects to local files, push local changes back
|
|
83
89
|
- **Change detection**: SHA256 hashing prevents unnecessary uploads
|
|
84
90
|
- **Multiple file types**: `.guidance` (AI prompts) and `.jinja` (NSL templates)
|
|
91
|
+
- **Project metadata**: Each project includes `metadata.json` with complete project info
|
|
85
92
|
- **AKB import**: Import knowledge base articles from structured text files
|
|
86
93
|
- **Project structure export**: Generates `flows.yaml` with complete project metadata
|
|
87
94
|
- **Robust authentication**: API key exchange with automatic token refresh
|
|
@@ -95,8 +102,8 @@ on:
|
|
|
95
102
|
push:
|
|
96
103
|
branches: [ main ]
|
|
97
104
|
paths:
|
|
98
|
-
- '
|
|
99
|
-
- '
|
|
105
|
+
- 'projects/**/*.guidance'
|
|
106
|
+
- 'projects/**/*.jinja'
|
|
100
107
|
jobs:
|
|
101
108
|
deploy:
|
|
102
109
|
runs-on: ubuntu-latest
|
|
@@ -149,6 +156,8 @@ Each article will be imported with:
|
|
|
149
156
|
Use `--verbose` flag to see detailed import progress.
|
|
150
157
|
|
|
151
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
|
|
152
161
|
- `GET /api/v1/bff/agents/list?project_id=...` - List project agents
|
|
153
162
|
- `GET /api/v1/designer/flows/{flowId}/skills` - List skills in flow
|
|
154
163
|
- `GET /api/v1/designer/skills/{skillId}` - Get skill content
|
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, import AKB articles",
|
|
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"
|
|
@@ -22,7 +22,9 @@
|
|
|
22
22
|
"local-development",
|
|
23
23
|
"akb",
|
|
24
24
|
"knowledge-base",
|
|
25
|
-
"import"
|
|
25
|
+
"import",
|
|
26
|
+
"multi-project",
|
|
27
|
+
"workspace"
|
|
26
28
|
],
|
|
27
29
|
"author": "sabbah13",
|
|
28
30
|
"license": "MIT",
|
|
@@ -44,10 +46,17 @@
|
|
|
44
46
|
"js-yaml": "^4.1.0",
|
|
45
47
|
"minimist": "^1.2.8"
|
|
46
48
|
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"mocha": "^10.2.0"
|
|
51
|
+
},
|
|
47
52
|
"scripts": {
|
|
48
53
|
"dev": "node ./src/cli.js",
|
|
49
54
|
"pull": "node ./src/cli.js pull",
|
|
50
55
|
"push": "node ./src/cli.js push",
|
|
51
|
-
"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"
|
|
52
61
|
}
|
|
53
62
|
}
|
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;
|
package/src/cli.js
CHANGED
|
@@ -17,17 +17,24 @@ async function main() {
|
|
|
17
17
|
if (!cmd || ['help', '-h', '--help'].includes(cmd)) {
|
|
18
18
|
console.log(`NEWO CLI
|
|
19
19
|
Usage:
|
|
20
|
-
newo pull # download
|
|
20
|
+
newo pull # download all projects -> ./projects/ OR specific project if NEWO_PROJECT_ID set
|
|
21
21
|
newo push # upload modified *.guidance/*.jinja back to NEWO
|
|
22
22
|
newo status # show modified files
|
|
23
|
-
newo meta # get project metadata (debug)
|
|
23
|
+
newo meta # get project metadata (debug, requires NEWO_PROJECT_ID)
|
|
24
24
|
newo import-akb <file> <persona_id> # import AKB articles from file
|
|
25
25
|
|
|
26
26
|
Flags:
|
|
27
27
|
--verbose, -v # enable detailed logging
|
|
28
28
|
|
|
29
29
|
Env:
|
|
30
|
-
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
|
|
31
38
|
`);
|
|
32
39
|
return;
|
|
33
40
|
}
|
|
@@ -35,8 +42,8 @@ Env:
|
|
|
35
42
|
const client = await makeClient(verbose);
|
|
36
43
|
|
|
37
44
|
if (cmd === 'pull') {
|
|
38
|
-
|
|
39
|
-
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);
|
|
40
47
|
} else if (cmd === 'push') {
|
|
41
48
|
await pushChanged(client, verbose);
|
|
42
49
|
} else if (cmd === 'status') {
|
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
|
}
|