newo 1.3.0 → 1.5.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 +99 -2
- package/README.md +59 -10
- package/dist/akb.d.ts +10 -0
- package/dist/akb.js +84 -0
- package/dist/api.d.ts +13 -0
- package/dist/api.js +100 -0
- package/dist/auth.d.ts +6 -0
- package/dist/auth.js +104 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +111 -0
- package/dist/fsutil.d.ts +12 -0
- package/dist/fsutil.js +28 -0
- package/dist/hash.d.ts +5 -0
- package/dist/hash.js +17 -0
- package/dist/sync.d.ts +7 -0
- package/dist/sync.js +337 -0
- package/dist/types.d.ts +206 -0
- package/dist/types.js +5 -0
- package/package.json +32 -9
- package/src/{akb.js → akb.ts} +16 -25
- package/src/api.ts +127 -0
- package/src/auth.ts +142 -0
- package/src/{cli.js → cli.ts} +29 -15
- package/src/fsutil.ts +41 -0
- package/src/hash.ts +20 -0
- package/src/sync.ts +396 -0
- package/src/types.ts +248 -0
- package/src/api.js +0 -98
- package/src/auth.js +0 -92
- package/src/fsutil.js +0 -26
- package/src/hash.js +0 -17
- package/src/sync.js +0 -284
package/src/{cli.js → cli.ts}
RENAMED
|
@@ -5,29 +5,37 @@ import { makeClient, getProjectMeta, importAkbArticle } from './api.js';
|
|
|
5
5
|
import { pullAll, pushChanged, status } from './sync.js';
|
|
6
6
|
import { parseAkbFile, prepareArticlesForImport } from './akb.js';
|
|
7
7
|
import path from 'path';
|
|
8
|
+
import type { NewoEnvironment, CliArgs, NewoApiError } from './types.js';
|
|
8
9
|
|
|
9
10
|
dotenv.config();
|
|
10
|
-
const { NEWO_PROJECT_ID } = process.env;
|
|
11
|
+
const { NEWO_PROJECT_ID } = process.env as NewoEnvironment;
|
|
11
12
|
|
|
12
|
-
async function main() {
|
|
13
|
-
const args = minimist(process.argv.slice(2));
|
|
13
|
+
async function main(): Promise<void> {
|
|
14
|
+
const args = minimist(process.argv.slice(2)) as CliArgs;
|
|
14
15
|
const cmd = args._[0];
|
|
15
|
-
const verbose = args.verbose || args.v;
|
|
16
|
+
const verbose = Boolean(args.verbose || args.v);
|
|
16
17
|
|
|
17
18
|
if (!cmd || ['help', '-h', '--help'].includes(cmd)) {
|
|
18
19
|
console.log(`NEWO CLI
|
|
19
20
|
Usage:
|
|
20
|
-
newo pull # download
|
|
21
|
+
newo pull # download all projects -> ./projects/ OR specific project if NEWO_PROJECT_ID set
|
|
21
22
|
newo push # upload modified *.guidance/*.jinja back to NEWO
|
|
22
23
|
newo status # show modified files
|
|
23
|
-
newo meta # get project metadata (debug)
|
|
24
|
+
newo meta # get project metadata (debug, requires NEWO_PROJECT_ID)
|
|
24
25
|
newo import-akb <file> <persona_id> # import AKB articles from file
|
|
25
26
|
|
|
26
27
|
Flags:
|
|
27
28
|
--verbose, -v # enable detailed logging
|
|
28
29
|
|
|
29
30
|
Env:
|
|
30
|
-
NEWO_BASE_URL, NEWO_PROJECT_ID, NEWO_API_KEY, NEWO_REFRESH_URL (optional)
|
|
31
|
+
NEWO_BASE_URL, NEWO_PROJECT_ID (optional), NEWO_API_KEY, NEWO_REFRESH_URL (optional)
|
|
32
|
+
|
|
33
|
+
Notes:
|
|
34
|
+
- multi-project support: pull downloads all accessible projects or single project based on NEWO_PROJECT_ID
|
|
35
|
+
- If NEWO_PROJECT_ID is set, pull downloads only that project
|
|
36
|
+
- If NEWO_PROJECT_ID is not set, pull downloads all projects accessible with your API key
|
|
37
|
+
- Projects are stored in ./projects/{project-idn}/ folders
|
|
38
|
+
- Each project folder contains metadata.json and flows.yaml
|
|
31
39
|
`);
|
|
32
40
|
return;
|
|
33
41
|
}
|
|
@@ -35,14 +43,16 @@ Env:
|
|
|
35
43
|
const client = await makeClient(verbose);
|
|
36
44
|
|
|
37
45
|
if (cmd === 'pull') {
|
|
38
|
-
|
|
39
|
-
await pullAll(client, NEWO_PROJECT_ID, verbose);
|
|
46
|
+
// If PROJECT_ID is set, pull single project; otherwise pull all projects
|
|
47
|
+
await pullAll(client, NEWO_PROJECT_ID || null, verbose);
|
|
40
48
|
} else if (cmd === 'push') {
|
|
41
49
|
await pushChanged(client, verbose);
|
|
42
50
|
} else if (cmd === 'status') {
|
|
43
51
|
await status(verbose);
|
|
44
52
|
} else if (cmd === 'meta') {
|
|
45
|
-
if (!NEWO_PROJECT_ID)
|
|
53
|
+
if (!NEWO_PROJECT_ID) {
|
|
54
|
+
throw new Error('NEWO_PROJECT_ID is not set in env');
|
|
55
|
+
}
|
|
46
56
|
const meta = await getProjectMeta(client, NEWO_PROJECT_ID);
|
|
47
57
|
console.log(JSON.stringify(meta, null, 2));
|
|
48
58
|
} else if (cmd === 'import-akb') {
|
|
@@ -72,13 +82,16 @@ Env:
|
|
|
72
82
|
|
|
73
83
|
for (const [index, article] of preparedArticles.entries()) {
|
|
74
84
|
try {
|
|
75
|
-
if (verbose)
|
|
85
|
+
if (verbose) {
|
|
86
|
+
console.log(` [${index + 1}/${preparedArticles.length}] Importing ${article.topic_name}...`);
|
|
87
|
+
}
|
|
76
88
|
await importAkbArticle(client, article);
|
|
77
89
|
successCount++;
|
|
78
90
|
if (!verbose) process.stdout.write('.');
|
|
79
91
|
} catch (error) {
|
|
80
92
|
errorCount++;
|
|
81
|
-
|
|
93
|
+
const errorMessage = (error as NewoApiError)?.response?.data || (error as Error).message;
|
|
94
|
+
console.error(`\n❌ Failed to import ${article.topic_name}:`, errorMessage);
|
|
82
95
|
}
|
|
83
96
|
}
|
|
84
97
|
|
|
@@ -86,7 +99,7 @@ Env:
|
|
|
86
99
|
console.log(`✅ Import complete: ${successCount} successful, ${errorCount} failed`);
|
|
87
100
|
|
|
88
101
|
} catch (error) {
|
|
89
|
-
console.error('❌ AKB import failed:', error.message);
|
|
102
|
+
console.error('❌ AKB import failed:', (error as Error).message);
|
|
90
103
|
process.exit(1);
|
|
91
104
|
}
|
|
92
105
|
} else {
|
|
@@ -95,7 +108,8 @@ Env:
|
|
|
95
108
|
}
|
|
96
109
|
}
|
|
97
110
|
|
|
98
|
-
main().catch((
|
|
99
|
-
|
|
111
|
+
main().catch((error: NewoApiError | Error) => {
|
|
112
|
+
const errorData = 'response' in error ? error?.response?.data : error;
|
|
113
|
+
console.error(errorData || error);
|
|
100
114
|
process.exit(1);
|
|
101
115
|
});
|
package/src/fsutil.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import type { RunnerType } from './types.js';
|
|
4
|
+
|
|
5
|
+
export const ROOT_DIR = path.join(process.cwd(), 'projects');
|
|
6
|
+
export const STATE_DIR = path.join(process.cwd(), '.newo');
|
|
7
|
+
export const MAP_PATH = path.join(STATE_DIR, 'map.json');
|
|
8
|
+
export const HASHES_PATH = path.join(STATE_DIR, 'hashes.json');
|
|
9
|
+
|
|
10
|
+
export async function ensureState(): Promise<void> {
|
|
11
|
+
await fs.ensureDir(STATE_DIR);
|
|
12
|
+
await fs.ensureDir(ROOT_DIR);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function projectDir(projectIdn: string): string {
|
|
16
|
+
return path.join(ROOT_DIR, projectIdn);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function skillPath(
|
|
20
|
+
projectIdn: string,
|
|
21
|
+
agentIdn: string,
|
|
22
|
+
flowIdn: string,
|
|
23
|
+
skillIdn: string,
|
|
24
|
+
runnerType: RunnerType = 'guidance'
|
|
25
|
+
): string {
|
|
26
|
+
const extension = runnerType === 'nsl' ? '.jinja' : '.guidance';
|
|
27
|
+
return path.join(ROOT_DIR, projectIdn, agentIdn, flowIdn, `${skillIdn}${extension}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function metadataPath(projectIdn: string): string {
|
|
31
|
+
return path.join(ROOT_DIR, projectIdn, 'metadata.json');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function writeFileAtomic(filepath: string, content: string): Promise<void> {
|
|
35
|
+
await fs.ensureDir(path.dirname(filepath));
|
|
36
|
+
await fs.writeFile(filepath, content, 'utf8');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function readIfExists(filepath: string): Promise<string | null> {
|
|
40
|
+
return (await fs.pathExists(filepath)) ? fs.readFile(filepath, 'utf8') : null;
|
|
41
|
+
}
|
package/src/hash.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { ensureState, HASHES_PATH } from './fsutil.js';
|
|
4
|
+
import type { HashStore } from './types.js';
|
|
5
|
+
|
|
6
|
+
export function sha256(str: string): string {
|
|
7
|
+
return crypto.createHash('sha256').update(str, 'utf8').digest('hex');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function loadHashes(): Promise<HashStore> {
|
|
11
|
+
await ensureState();
|
|
12
|
+
if (await fs.pathExists(HASHES_PATH)) {
|
|
13
|
+
return fs.readJson(HASHES_PATH) as Promise<HashStore>;
|
|
14
|
+
}
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function saveHashes(hashes: HashStore): Promise<void> {
|
|
19
|
+
await fs.writeJson(HASHES_PATH, hashes, { spaces: 2 });
|
|
20
|
+
}
|
package/src/sync.ts
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import {
|
|
2
|
+
listProjects,
|
|
3
|
+
listAgents,
|
|
4
|
+
listFlowSkills,
|
|
5
|
+
updateSkill,
|
|
6
|
+
listFlowEvents,
|
|
7
|
+
listFlowStates,
|
|
8
|
+
getProjectMeta
|
|
9
|
+
} from './api.js';
|
|
10
|
+
import {
|
|
11
|
+
ensureState,
|
|
12
|
+
skillPath,
|
|
13
|
+
writeFileAtomic,
|
|
14
|
+
readIfExists,
|
|
15
|
+
MAP_PATH,
|
|
16
|
+
projectDir,
|
|
17
|
+
metadataPath
|
|
18
|
+
} from './fsutil.js';
|
|
19
|
+
import fs from 'fs-extra';
|
|
20
|
+
import { sha256, loadHashes, saveHashes } from './hash.js';
|
|
21
|
+
import yaml from 'js-yaml';
|
|
22
|
+
import path from 'path';
|
|
23
|
+
import type { AxiosInstance } from 'axios';
|
|
24
|
+
import type {
|
|
25
|
+
Agent,
|
|
26
|
+
ProjectData,
|
|
27
|
+
ProjectMap,
|
|
28
|
+
LegacyProjectMap,
|
|
29
|
+
HashStore,
|
|
30
|
+
FlowsYamlData,
|
|
31
|
+
FlowsYamlAgent,
|
|
32
|
+
FlowsYamlFlow,
|
|
33
|
+
FlowsYamlSkill,
|
|
34
|
+
FlowsYamlEvent,
|
|
35
|
+
FlowsYamlState
|
|
36
|
+
} from './types.js';
|
|
37
|
+
|
|
38
|
+
export async function pullSingleProject(
|
|
39
|
+
client: AxiosInstance,
|
|
40
|
+
projectId: string,
|
|
41
|
+
projectIdn: string,
|
|
42
|
+
verbose: boolean = false
|
|
43
|
+
): Promise<ProjectData> {
|
|
44
|
+
if (verbose) console.log(`🔍 Fetching agents for project ${projectId} (${projectIdn})...`);
|
|
45
|
+
const agents = await listAgents(client, projectId);
|
|
46
|
+
if (verbose) console.log(`📦 Found ${agents.length} agents`);
|
|
47
|
+
|
|
48
|
+
// Get and save project metadata
|
|
49
|
+
const projectMeta = await getProjectMeta(client, projectId);
|
|
50
|
+
await writeFileAtomic(metadataPath(projectIdn), JSON.stringify(projectMeta, null, 2));
|
|
51
|
+
if (verbose) console.log(`✓ Saved metadata for ${projectIdn}`);
|
|
52
|
+
|
|
53
|
+
const projectMap: ProjectData = { projectId, projectIdn, agents: {} };
|
|
54
|
+
|
|
55
|
+
for (const agent of agents) {
|
|
56
|
+
const aKey = agent.idn;
|
|
57
|
+
projectMap.agents[aKey] = { id: agent.id, flows: {} };
|
|
58
|
+
|
|
59
|
+
for (const flow of agent.flows ?? []) {
|
|
60
|
+
projectMap.agents[aKey]!.flows[flow.idn] = { id: flow.id, skills: {} };
|
|
61
|
+
|
|
62
|
+
const skills = await listFlowSkills(client, flow.id);
|
|
63
|
+
for (const skill of skills) {
|
|
64
|
+
const file = skillPath(projectIdn, agent.idn, flow.idn, skill.idn, skill.runner_type);
|
|
65
|
+
await writeFileAtomic(file, skill.prompt_script || '');
|
|
66
|
+
|
|
67
|
+
// Store complete skill metadata for push operations
|
|
68
|
+
projectMap.agents[aKey]!.flows[flow.idn]!.skills[skill.idn] = {
|
|
69
|
+
id: skill.id,
|
|
70
|
+
title: skill.title,
|
|
71
|
+
idn: skill.idn,
|
|
72
|
+
runner_type: skill.runner_type,
|
|
73
|
+
model: skill.model,
|
|
74
|
+
parameters: skill.parameters,
|
|
75
|
+
path: skill.path || undefined
|
|
76
|
+
};
|
|
77
|
+
console.log(`✓ Pulled ${file}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Generate flows.yaml for this project
|
|
83
|
+
if (verbose) console.log(`📄 Generating flows.yaml for ${projectIdn}...`);
|
|
84
|
+
await generateFlowsYaml(client, agents, projectIdn, verbose);
|
|
85
|
+
|
|
86
|
+
return projectMap;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function pullAll(
|
|
90
|
+
client: AxiosInstance,
|
|
91
|
+
projectId: string | null = null,
|
|
92
|
+
verbose: boolean = false
|
|
93
|
+
): Promise<void> {
|
|
94
|
+
await ensureState();
|
|
95
|
+
|
|
96
|
+
if (projectId) {
|
|
97
|
+
// Single project mode
|
|
98
|
+
const projectMeta = await getProjectMeta(client, projectId);
|
|
99
|
+
const projectMap = await pullSingleProject(client, projectId, projectMeta.idn, verbose);
|
|
100
|
+
|
|
101
|
+
const idMap: ProjectMap = { projects: { [projectMeta.idn]: projectMap } };
|
|
102
|
+
await fs.writeJson(MAP_PATH, idMap, { spaces: 2 });
|
|
103
|
+
|
|
104
|
+
// Generate hash tracking for this project
|
|
105
|
+
const hashes: HashStore = {};
|
|
106
|
+
for (const [agentIdn, agentObj] of Object.entries(projectMap.agents)) {
|
|
107
|
+
for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
|
|
108
|
+
for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
|
|
109
|
+
const p = skillPath(projectMeta.idn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
110
|
+
const content = await fs.readFile(p, 'utf8');
|
|
111
|
+
hashes[p] = sha256(content);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
await saveHashes(hashes);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Multi-project mode
|
|
120
|
+
if (verbose) console.log('🔍 Fetching all projects...');
|
|
121
|
+
const projects = await listProjects(client);
|
|
122
|
+
if (verbose) console.log(`📦 Found ${projects.length} projects`);
|
|
123
|
+
|
|
124
|
+
const idMap: ProjectMap = { projects: {} };
|
|
125
|
+
const allHashes: HashStore = {};
|
|
126
|
+
|
|
127
|
+
for (const project of projects) {
|
|
128
|
+
if (verbose) console.log(`\n📁 Processing project: ${project.idn} (${project.title})`);
|
|
129
|
+
const projectMap = await pullSingleProject(client, project.id, project.idn, verbose);
|
|
130
|
+
idMap.projects[project.idn] = projectMap;
|
|
131
|
+
|
|
132
|
+
// Collect hashes for this project
|
|
133
|
+
for (const [agentIdn, agentObj] of Object.entries(projectMap.agents)) {
|
|
134
|
+
for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
|
|
135
|
+
for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
|
|
136
|
+
const p = skillPath(project.idn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
137
|
+
const content = await fs.readFile(p, 'utf8');
|
|
138
|
+
allHashes[p] = sha256(content);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
await fs.writeJson(MAP_PATH, idMap, { spaces: 2 });
|
|
145
|
+
await saveHashes(allHashes);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function pushChanged(client: AxiosInstance, verbose: boolean = false): Promise<void> {
|
|
149
|
+
await ensureState();
|
|
150
|
+
if (!(await fs.pathExists(MAP_PATH))) {
|
|
151
|
+
throw new Error('Missing .newo/map.json. Run `newo pull` first.');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (verbose) console.log('📋 Loading project mapping...');
|
|
155
|
+
const idMap = await fs.readJson(MAP_PATH) as ProjectMap | LegacyProjectMap;
|
|
156
|
+
if (verbose) console.log('🔍 Loading file hashes...');
|
|
157
|
+
const oldHashes = await loadHashes();
|
|
158
|
+
const newHashes: HashStore = { ...oldHashes };
|
|
159
|
+
|
|
160
|
+
if (verbose) console.log('🔄 Scanning for changes...');
|
|
161
|
+
let pushed = 0;
|
|
162
|
+
let scanned = 0;
|
|
163
|
+
|
|
164
|
+
// Handle both old single-project format and new multi-project format
|
|
165
|
+
const projects = 'projects' in idMap && idMap.projects ? idMap.projects : { '': idMap as ProjectData };
|
|
166
|
+
|
|
167
|
+
for (const [projectIdn, projectData] of Object.entries(projects)) {
|
|
168
|
+
if (verbose && projectIdn) console.log(`📁 Scanning project: ${projectIdn}`);
|
|
169
|
+
|
|
170
|
+
for (const [agentIdn, agentObj] of Object.entries(projectData.agents)) {
|
|
171
|
+
if (verbose) console.log(` 📁 Scanning agent: ${agentIdn}`);
|
|
172
|
+
for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
|
|
173
|
+
if (verbose) console.log(` 📁 Scanning flow: ${flowIdn}`);
|
|
174
|
+
for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
|
|
175
|
+
const p = projectIdn ?
|
|
176
|
+
skillPath(projectIdn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type) :
|
|
177
|
+
skillPath('', agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
178
|
+
scanned++;
|
|
179
|
+
if (verbose) console.log(` 📄 Checking: ${p}`);
|
|
180
|
+
|
|
181
|
+
const content = await readIfExists(p);
|
|
182
|
+
if (content === null) {
|
|
183
|
+
if (verbose) console.log(` ⚠️ File not found: ${p}`);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const h = sha256(content);
|
|
188
|
+
const oldHash = oldHashes[p];
|
|
189
|
+
if (verbose) {
|
|
190
|
+
console.log(` 🔍 Hash comparison:`);
|
|
191
|
+
console.log(` Old: ${oldHash || 'none'}`);
|
|
192
|
+
console.log(` New: ${h}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (oldHash !== h) {
|
|
196
|
+
if (verbose) console.log(` 🔄 File changed, preparing to push...`);
|
|
197
|
+
|
|
198
|
+
// Create complete skill object with updated prompt_script
|
|
199
|
+
const skillObject = {
|
|
200
|
+
id: skillMeta.id,
|
|
201
|
+
title: skillMeta.title,
|
|
202
|
+
idn: skillMeta.idn,
|
|
203
|
+
prompt_script: content,
|
|
204
|
+
runner_type: skillMeta.runner_type,
|
|
205
|
+
model: skillMeta.model,
|
|
206
|
+
parameters: skillMeta.parameters,
|
|
207
|
+
path: skillMeta.path || undefined
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
if (verbose) {
|
|
211
|
+
console.log(` 📤 Pushing skill object:`);
|
|
212
|
+
console.log(` ID: ${skillObject.id}`);
|
|
213
|
+
console.log(` Title: ${skillObject.title}`);
|
|
214
|
+
console.log(` IDN: ${skillObject.idn}`);
|
|
215
|
+
console.log(` Content length: ${content.length} chars`);
|
|
216
|
+
console.log(` Content preview: ${content.substring(0, 100).replace(/\n/g, '\\n')}...`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
await updateSkill(client, skillObject);
|
|
220
|
+
console.log(`↑ Pushed ${p}`);
|
|
221
|
+
newHashes[p] = h;
|
|
222
|
+
pushed++;
|
|
223
|
+
} else if (verbose) {
|
|
224
|
+
console.log(` ✓ No changes`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (verbose) console.log(`🔄 Scanned ${scanned} files, found ${pushed} changes`);
|
|
232
|
+
await saveHashes(newHashes);
|
|
233
|
+
console.log(pushed ? `✅ Push complete. ${pushed} file(s) updated.` : '✅ Nothing to push.');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export async function status(verbose: boolean = false): Promise<void> {
|
|
237
|
+
await ensureState();
|
|
238
|
+
if (!(await fs.pathExists(MAP_PATH))) {
|
|
239
|
+
console.log('No map. Run `newo pull` first.');
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (verbose) console.log('📋 Loading project mapping and hashes...');
|
|
244
|
+
const idMap = await fs.readJson(MAP_PATH) as ProjectMap | LegacyProjectMap;
|
|
245
|
+
const hashes = await loadHashes();
|
|
246
|
+
let dirty = 0;
|
|
247
|
+
|
|
248
|
+
// Handle both old single-project format and new multi-project format
|
|
249
|
+
const projects = 'projects' in idMap && idMap.projects ? idMap.projects : { '': idMap as ProjectData };
|
|
250
|
+
|
|
251
|
+
for (const [projectIdn, projectData] of Object.entries(projects)) {
|
|
252
|
+
if (verbose && projectIdn) console.log(`📁 Checking project: ${projectIdn}`);
|
|
253
|
+
|
|
254
|
+
for (const [agentIdn, agentObj] of Object.entries(projectData.agents)) {
|
|
255
|
+
if (verbose) console.log(` 📁 Checking agent: ${agentIdn}`);
|
|
256
|
+
for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
|
|
257
|
+
if (verbose) console.log(` 📁 Checking flow: ${flowIdn}`);
|
|
258
|
+
for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
|
|
259
|
+
const p = projectIdn ?
|
|
260
|
+
skillPath(projectIdn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type) :
|
|
261
|
+
skillPath('', agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
262
|
+
const exists = await fs.pathExists(p);
|
|
263
|
+
if (!exists) {
|
|
264
|
+
console.log(`D ${p}`);
|
|
265
|
+
dirty++;
|
|
266
|
+
if (verbose) console.log(` ❌ Deleted: ${p}`);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const content = await fs.readFile(p, 'utf8');
|
|
270
|
+
const h = sha256(content);
|
|
271
|
+
const oldHash = hashes[p];
|
|
272
|
+
if (verbose) {
|
|
273
|
+
console.log(` 📄 ${p}`);
|
|
274
|
+
console.log(` Old hash: ${oldHash || 'none'}`);
|
|
275
|
+
console.log(` New hash: ${h}`);
|
|
276
|
+
}
|
|
277
|
+
if (oldHash !== h) {
|
|
278
|
+
console.log(`M ${p}`);
|
|
279
|
+
dirty++;
|
|
280
|
+
if (verbose) console.log(` 🔄 Modified: ${p}`);
|
|
281
|
+
} else if (verbose) {
|
|
282
|
+
console.log(` ✓ Unchanged: ${p}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
console.log(dirty ? `${dirty} changed file(s).` : 'Clean.');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function generateFlowsYaml(
|
|
292
|
+
client: AxiosInstance,
|
|
293
|
+
agents: Agent[],
|
|
294
|
+
projectIdn: string,
|
|
295
|
+
verbose: boolean = false
|
|
296
|
+
): Promise<void> {
|
|
297
|
+
const flowsData: FlowsYamlData = { flows: [] };
|
|
298
|
+
|
|
299
|
+
for (const agent of agents) {
|
|
300
|
+
if (verbose) console.log(` 📁 Processing agent: ${agent.idn}`);
|
|
301
|
+
|
|
302
|
+
const agentFlows: FlowsYamlFlow[] = [];
|
|
303
|
+
|
|
304
|
+
for (const flow of agent.flows ?? []) {
|
|
305
|
+
if (verbose) console.log(` 📄 Processing flow: ${flow.idn}`);
|
|
306
|
+
|
|
307
|
+
// Get skills for this flow
|
|
308
|
+
const skills = await listFlowSkills(client, flow.id);
|
|
309
|
+
const skillsData: FlowsYamlSkill[] = skills.map(skill => ({
|
|
310
|
+
idn: skill.idn,
|
|
311
|
+
title: skill.title || "",
|
|
312
|
+
prompt_script: `flows/${flow.idn}/${skill.idn}.${skill.runner_type === 'nsl' ? 'jinja' : 'nsl'}`,
|
|
313
|
+
runner_type: `!enum "RunnerType.${skill.runner_type}"`,
|
|
314
|
+
model: {
|
|
315
|
+
model_idn: skill.model.model_idn,
|
|
316
|
+
provider_idn: skill.model.provider_idn
|
|
317
|
+
},
|
|
318
|
+
parameters: skill.parameters.map(param => ({
|
|
319
|
+
name: param.name,
|
|
320
|
+
default_value: param.default_value || " "
|
|
321
|
+
}))
|
|
322
|
+
}));
|
|
323
|
+
|
|
324
|
+
// Get events for this flow
|
|
325
|
+
let eventsData: FlowsYamlEvent[] = [];
|
|
326
|
+
try {
|
|
327
|
+
const events = await listFlowEvents(client, flow.id);
|
|
328
|
+
eventsData = events.map(event => ({
|
|
329
|
+
title: event.description,
|
|
330
|
+
idn: event.idn,
|
|
331
|
+
skill_selector: `!enum "SkillSelector.${event.skill_selector}"`,
|
|
332
|
+
skill_idn: event.skill_idn || undefined,
|
|
333
|
+
state_idn: event.state_idn || undefined,
|
|
334
|
+
integration_idn: event.integration_idn || undefined,
|
|
335
|
+
connector_idn: event.connector_idn || undefined,
|
|
336
|
+
interrupt_mode: `!enum "InterruptMode.${event.interrupt_mode}"`
|
|
337
|
+
}));
|
|
338
|
+
if (verbose) console.log(` 📋 Found ${events.length} events`);
|
|
339
|
+
} catch (error) {
|
|
340
|
+
if (verbose) console.log(` ⚠️ No events found for flow ${flow.idn}`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Get state fields for this flow
|
|
344
|
+
let stateFieldsData: FlowsYamlState[] = [];
|
|
345
|
+
try {
|
|
346
|
+
const states = await listFlowStates(client, flow.id);
|
|
347
|
+
stateFieldsData = states.map(state => ({
|
|
348
|
+
title: state.title,
|
|
349
|
+
idn: state.idn,
|
|
350
|
+
default_value: state.default_value || undefined,
|
|
351
|
+
scope: `!enum "StateFieldScope.${state.scope}"`
|
|
352
|
+
}));
|
|
353
|
+
if (verbose) console.log(` 📊 Found ${states.length} state fields`);
|
|
354
|
+
} catch (error) {
|
|
355
|
+
if (verbose) console.log(` ⚠️ No state fields found for flow ${flow.idn}`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
agentFlows.push({
|
|
359
|
+
idn: flow.idn,
|
|
360
|
+
title: flow.title,
|
|
361
|
+
description: flow.description || null,
|
|
362
|
+
default_runner_type: `!enum "RunnerType.${flow.default_runner_type}"`,
|
|
363
|
+
default_provider_idn: flow.default_model.provider_idn,
|
|
364
|
+
default_model_idn: flow.default_model.model_idn,
|
|
365
|
+
skills: skillsData,
|
|
366
|
+
events: eventsData,
|
|
367
|
+
state_fields: stateFieldsData
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const agentData: FlowsYamlAgent = {
|
|
372
|
+
agent_idn: agent.idn,
|
|
373
|
+
agent_description: agent.description || undefined,
|
|
374
|
+
agent_flows: agentFlows
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
flowsData.flows.push(agentData);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Convert to YAML and write to file with custom enum handling
|
|
381
|
+
let yamlContent = yaml.dump(flowsData, {
|
|
382
|
+
indent: 2,
|
|
383
|
+
lineWidth: -1,
|
|
384
|
+
noRefs: true,
|
|
385
|
+
sortKeys: false,
|
|
386
|
+
quotingType: '"',
|
|
387
|
+
forceQuotes: false
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// Post-process to fix enum formatting
|
|
391
|
+
yamlContent = yamlContent.replace(/"(!enum "[^"]+")"/g, '$1');
|
|
392
|
+
|
|
393
|
+
const yamlPath = path.join(projectDir(projectIdn), 'flows.yaml');
|
|
394
|
+
await writeFileAtomic(yamlPath, yamlContent);
|
|
395
|
+
console.log(`✓ Generated flows.yaml for ${projectIdn}`);
|
|
396
|
+
}
|