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 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.3.0] - 2025-01-21
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-01-20
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 (advanced):
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 project -> ./project
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
- - `./project/<AgentIdn>/<FlowIdn>/<SkillIdn>.guidance` (AI guidance scripts)
76
- - `./project/<AgentIdn>/<FlowIdn>/<SkillIdn>.jinja` (NSL/Jinja template scripts)
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
- - 'project/**/*.guidance'
99
- - 'project/**/*.jinja'
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.3.0",
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 project -> ./project
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
- if (!NEWO_PROJECT_ID) throw new Error('NEWO_PROJECT_ID is not set in env');
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(), 'project');
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 skillPath(agentIdn, flowIdn, skillIdn, runnerType = 'guidance') {
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 pullAll(client, projectId, verbose = false) {
9
- await ensureState();
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
- const idMap = { projectId, agents: {} };
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
- idMap.agents[aKey] = { id: agent.id, flows: {} };
22
+ projectMap.agents[aKey] = { id: agent.id, flows: {} };
19
23
 
20
24
  for (const flow of agent.flows ?? []) {
21
- idMap.agents[aKey].flows[flow.idn] = { id: flow.id, skills: {} };
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
- idMap.agents[aKey].flows[flow.idn].skills[s.idn] = {
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
- await fs.writeJson(MAP_PATH, idMap, { spaces: 2 });
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
- // Generate flows.yaml
45
- if (verbose) console.log('📄 Generating flows.yaml...');
46
- await generateFlowsYaml(client, agents, verbose);
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
- const hashes = {};
49
- for (const [agentIdn, agentObj] of Object.entries(idMap.agents)) {
50
- for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
51
- for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
52
- const p = skillPath(agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
53
- const content = await fs.readFile(p, 'utf8');
54
- hashes[p] = sha256(content);
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
- await saveHashes(hashes);
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
- for (const [agentIdn, agentObj] of Object.entries(idMap.agents)) {
78
- if (verbose) console.log(` 📁 Scanning agent: ${agentIdn}`);
79
- for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
80
- if (verbose) console.log(` 📁 Scanning flow: ${flowIdn}`);
81
- for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
82
- const p = skillPath(agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
83
- scanned++;
84
- if (verbose) console.log(` 📄 Checking: ${p}`);
85
-
86
- const content = await readIfExists(p);
87
- if (content === null) {
88
- if (verbose) console.log(` ⚠️ File not found: ${p}`);
89
- continue;
90
- }
91
-
92
- const h = sha256(content);
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
- // Create complete skill object with updated prompt_script
104
- const skillObject = {
105
- id: skillMeta.id,
106
- title: skillMeta.title,
107
- idn: skillMeta.idn,
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(` 📤 Pushing skill object:`);
117
- console.log(` ID: ${skillObject.id}`);
118
- console.log(` Title: ${skillObject.title}`);
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
- await updateSkill(client, skillObject);
125
- console.log(`↑ Pushed ${p}`);
126
- newHashes[p] = h;
127
- pushed++;
128
- } else if (verbose) {
129
- console.log(` ✓ No changes`);
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
- for (const [agentIdn, agentObj] of Object.entries(idMap.agents)) {
153
- if (verbose) console.log(` 📁 Checking agent: ${agentIdn}`);
154
- for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
155
- if (verbose) console.log(` 📁 Checking flow: ${flowIdn}`);
156
- for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
157
- const p = skillPath(agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
158
- const exists = await fs.pathExists(p);
159
- if (!exists) {
160
- console.log(`D ${p}`);
161
- dirty++;
162
- if (verbose) console.log(` ❌ Deleted: ${p}`);
163
- continue;
164
- }
165
- const content = await fs.readFile(p, 'utf8');
166
- const h = sha256(content);
167
- const oldHash = hashes[p];
168
- if (verbose) {
169
- console.log(` 📄 ${p}`);
170
- console.log(` Old hash: ${oldHash || 'none'}`);
171
- console.log(` New hash: ${h}`);
172
- }
173
- if (oldHash !== h) {
174
- console.log(`M ${p}`);
175
- dirty++;
176
- if (verbose) console.log(` 🔄 Modified: ${p}`);
177
- } else if (verbose) {
178
- console.log(` Unchanged: ${p}`);
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 fs.writeFile(yamlPath, yamlContent, 'utf8');
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
  }