@lazyingart/agintiflow 0.20.190 → 0.20.192
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/README.md +6 -3
- package/docs/auxiliary-image-generation.md +3 -2
- package/docs/model-selection.md +2 -1
- package/docs/real-deepseek-capabilities.md +1 -1
- package/package.json +1 -1
- package/public/app.js +5 -5
- package/references/agintiflow-npm-publication-memory.md +11 -0
- package/references/lazyedit-publish-skill-installation-2026-06-03.md +88 -0
- package/references/paper-revision-skill-installation-2026-06-03.md +62 -0
- package/scripts/real-deepseek-capabilities.js +1 -1
- package/scripts/smoke-auth.js +34 -8
- package/scripts/smoke-auxiliary-tools.js +30 -1
- package/scripts/smoke-skillmesh.js +23 -0
- package/src/auth-onboarding.js +13 -11
- package/src/auxiliary-tools.js +29 -3
- package/src/capabilities.js +3 -1
- package/src/cli.js +19 -14
- package/src/i18n.js +1 -1
- package/src/interactive-cli.js +7 -4
- package/src/project.js +74 -36
- package/src/skill-library.js +11 -7
- package/src/skillmesh.js +56 -20
- package/web.js +5 -0
package/README.md
CHANGED
|
@@ -85,7 +85,7 @@ aginti init --template aaps
|
|
|
85
85
|
aginti init --template supervision
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
On first interactive use, AgInTiFlow opens an auth wizard if no main model key is found. Pick DeepSeek, OpenAI, OpenRouter, Qwen, or Venice, paste the key, and it saves to
|
|
88
|
+
On first interactive use, AgInTiFlow opens an auth wizard if no main model key is found. Pick DeepSeek, OpenAI, OpenRouter, Qwen, or Venice, paste the key, and it saves account-wide to `~/.agintiflow/.env` with restricted permissions. Current project `.aginti/.env` files can still override account defaults when needed. You can rerun setup any time:
|
|
89
89
|
|
|
90
90
|
```bash
|
|
91
91
|
aginti auth
|
|
@@ -93,6 +93,7 @@ aginti auth deepseek
|
|
|
93
93
|
aginti auth openrouter
|
|
94
94
|
aginti auth venice
|
|
95
95
|
aginti login grsai
|
|
96
|
+
aginti auth --project openai # project override in .aginti/.env
|
|
96
97
|
```
|
|
97
98
|
|
|
98
99
|
Provider signup and key pages:
|
|
@@ -328,9 +329,10 @@ AgInTiFlow stores canonical sessions centrally and keeps only project-local poin
|
|
|
328
329
|
|
|
329
330
|
| Location | Purpose |
|
|
330
331
|
| --- | --- |
|
|
332
|
+
| `~/.agintiflow/.env` | Account-wide provider keys usable from any project. Local to the user account. |
|
|
331
333
|
| `~/.agintiflow/sessions/<session-id>/` | Canonical state, events, browser state, artifacts, snapshots, canvas files. |
|
|
332
334
|
| `<project>/.aginti-sessions/` | Project-local session pointers and web UI database. Ignored by git. |
|
|
333
|
-
| `<project>/.aginti/.env` | Optional project-local API
|
|
335
|
+
| `<project>/.aginti/.env` | Optional project-local API key overrides with restricted permissions. Ignored by git. |
|
|
334
336
|
| `<project>/AGINTI.md` | Editable project instructions and durable local preferences. Safe to commit if it contains no secrets. |
|
|
335
337
|
|
|
336
338
|
Safety defaults:
|
|
@@ -366,13 +368,14 @@ PACKAGE_INSTALL_POLICY=allow
|
|
|
366
368
|
COMMAND_CWD=/path/to/project
|
|
367
369
|
```
|
|
368
370
|
|
|
369
|
-
|
|
371
|
+
Account-wide keys:
|
|
370
372
|
|
|
371
373
|
```bash
|
|
372
374
|
aginti init
|
|
373
375
|
printf '%s' "$DEEPSEEK_API_KEY" | aginti keys set deepseek --stdin
|
|
374
376
|
printf '%s' "$OPENROUTER_API_KEY" | aginti keys set openrouter --stdin
|
|
375
377
|
printf '%s' "$VENICE_API_KEY" | aginti keys set venice --stdin
|
|
378
|
+
printf '%s' "$OPENAI_API_KEY" | aginti keys set --project openai --stdin # optional project override
|
|
376
379
|
```
|
|
377
380
|
|
|
378
381
|
More detail:
|
|
@@ -13,7 +13,7 @@ editable vectors, use AgInTiFlow file tools to write deterministic SVG/LaTeX/HTM
|
|
|
13
13
|
|
|
14
14
|
## Setup
|
|
15
15
|
|
|
16
|
-
Store a GRS AI or Venice key
|
|
16
|
+
Store a GRS AI or Venice image key account-wide:
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
19
|
aginti login grsai
|
|
@@ -21,6 +21,7 @@ aginti login venice
|
|
|
21
21
|
# or
|
|
22
22
|
printf '%s' "$GRSAI" | aginti keys set grsai --stdin
|
|
23
23
|
printf '%s' "$VENICE_API_KEY" | aginti keys set venice --stdin
|
|
24
|
+
printf '%s' "$VENICE_API_KEY" | aginti keys set --project venice --stdin # optional project override
|
|
24
25
|
```
|
|
25
26
|
|
|
26
27
|
Inside interactive chat, use either spelling:
|
|
@@ -29,7 +30,7 @@ Inside interactive chat, use either spelling:
|
|
|
29
30
|
/auxiliary grsai
|
|
30
31
|
```
|
|
31
32
|
|
|
32
|
-
Keys are saved in
|
|
33
|
+
Keys are saved in `~/.agintiflow/.env` by default as `GRSAI` or `VENICE_API_KEY` with `0600` permissions. Project `.aginti/.env` can override them. The CLI and web app only report whether a key exists; they never return raw values.
|
|
33
34
|
|
|
34
35
|
## Runtime Flow
|
|
35
36
|
|
package/docs/model-selection.md
CHANGED
|
@@ -133,7 +133,7 @@ It keeps smart routing enabled. If the Venice key is missing, run `/auth venice`
|
|
|
133
133
|
|
|
134
134
|
## Keys
|
|
135
135
|
|
|
136
|
-
Keys are
|
|
136
|
+
Keys are saved account-wide in `~/.agintiflow/.env` by default. Use `--project` only when the current project needs an override in ignored `.aginti/.env`:
|
|
137
137
|
|
|
138
138
|
```bash
|
|
139
139
|
aginti auth
|
|
@@ -142,6 +142,7 @@ aginti auth venice
|
|
|
142
142
|
aginti keys status
|
|
143
143
|
printf '%s' "$OPENROUTER_API_KEY" | aginti keys set openrouter --stdin
|
|
144
144
|
printf '%s' "$VENICE_API_KEY" | aginti keys set venice --stdin
|
|
145
|
+
printf '%s' "$OPENAI_API_KEY" | aginti keys set --project openai --stdin
|
|
145
146
|
```
|
|
146
147
|
|
|
147
148
|
The web UI mirrors the same roles with dropdowns. Keep the left panel for daily controls: routing policy, provider/model, profile, goal, workspace, sandbox, and common tools. Use **Advanced settings** for model roles, browser start URL, wrapper/scout settings, Docker image, password/destructive toggles, and auxiliary image models. Runtime logs stay in the right column, followed by wrapper, workspace, and sandbox capability panels.
|
|
@@ -6,7 +6,7 @@ AgInTiFlow keeps deterministic mock smoke tests for CI, but Round 9 added an opt
|
|
|
6
6
|
|
|
7
7
|
- Node.js 22+
|
|
8
8
|
- A project folder initialized with `aginti init`
|
|
9
|
-
- `DEEPSEEK_API_KEY` in the environment or project-local `.aginti/.env`
|
|
9
|
+
- `DEEPSEEK_API_KEY` in the environment, account `~/.agintiflow/.env`, or project-local `.aginti/.env`
|
|
10
10
|
- Optional web UI running from the same project folder
|
|
11
11
|
|
|
12
12
|
Never print or commit API keys. The suite reports only provider availability.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.192",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AgInTiFlow is a project-aware agent workspace for hybrid wet-dry R&D, hardware-aware intelligence, software automation, and industrial workflows.",
|
|
6
6
|
"license": "Apache-2.0",
|
package/public/app.js
CHANGED
|
@@ -22,17 +22,17 @@ const translations = {
|
|
|
22
22
|
projectStatusTitle: "Project folder",
|
|
23
23
|
setupTitle: "Provider setup",
|
|
24
24
|
setupHelp:
|
|
25
|
-
"DeepSeek/OpenAI/Qwen/Venice keys are missing. Use mock mode, export an env var, or save
|
|
25
|
+
"DeepSeek/OpenAI/Qwen/Venice keys are missing. Use mock mode, export an env var, or save an account-wide model key.",
|
|
26
26
|
setupEnvHelp:
|
|
27
|
-
"Env vars: DEEPSEEK_API_KEY, OPENAI_API_KEY, QWEN_API_KEY, VENICE_API_KEY, LLM_API_KEY, and optional GRSAI for image generation.
|
|
27
|
+
"Env vars: DEEPSEEK_API_KEY, OPENAI_API_KEY, QWEN_API_KEY, VENICE_API_KEY, LLM_API_KEY, and optional GRSAI/VENICE_API_KEY for image generation. Saved keys live in ~/.agintiflow/.env; project .aginti/.env can override.",
|
|
28
28
|
setupKeyLinksLabel: "Get keys:",
|
|
29
29
|
deepseekKeyLink: "DeepSeek API keys",
|
|
30
30
|
openaiKeyLink: "OpenAI API keys",
|
|
31
31
|
setupProviderLabel: "Provider",
|
|
32
32
|
setupKeyLabel: "API key",
|
|
33
|
-
saveKeyButton: "Save
|
|
34
|
-
keySavedStatus: "
|
|
35
|
-
keySaveFailed: "Failed to save
|
|
33
|
+
saveKeyButton: "Save account key",
|
|
34
|
+
keySavedStatus: "Account key saved. Raw values are never returned by the API.",
|
|
35
|
+
keySaveFailed: "Failed to save account key.",
|
|
36
36
|
taskProfileLabel: "Task profile",
|
|
37
37
|
routingModeLabel: "Routing policy",
|
|
38
38
|
routingSmartOption: "Smart: flash/pro/wrappers",
|
|
@@ -10,6 +10,17 @@ This note records the working publication route for AgInTiFlow so future release
|
|
|
10
10
|
|
|
11
11
|
Latest verified release:
|
|
12
12
|
|
|
13
|
+
- Version: `0.20.190`
|
|
14
|
+
- Commit: `f192f8e`
|
|
15
|
+
- GitHub Actions run: `https://github.com/lazyingart/AgInTiFlow/actions/runs/26736284533`
|
|
16
|
+
- Workflow result: success
|
|
17
|
+
- npm registry check: `npm view @lazyingart/agintiflow version dist-tags.latest --registry=https://registry.npmjs.org` returned `0.20.190`.
|
|
18
|
+
- Installed verification: `npm install -g @lazyingart/agintiflow@0.20.190` then `aginti --version` returned `0.20.190`.
|
|
19
|
+
- Webapp verification: `aginti webapp restart --port 3210` then `curl -fsS http://127.0.0.1:3210/health` returned `version":"0.20.190"` from the global npm package path.
|
|
20
|
+
- Feature verification: `aginti --provider mock --routing manual --model mock-agent --no-shell --allow-file-tools --scs off "hello"` returned a direct greeting and did not create `hello.py`.
|
|
21
|
+
|
|
22
|
+
Previous verified release:
|
|
23
|
+
|
|
13
24
|
- Version: `0.20.189`
|
|
14
25
|
- Commit: `0095284`
|
|
15
26
|
- GitHub Actions run: `https://github.com/lazyingart/AgInTiFlow/actions/runs/26734901522`
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# LazyEdit Publish Skill Installation
|
|
2
|
+
|
|
3
|
+
Date: 2026-06-03
|
|
4
|
+
|
|
5
|
+
## Installed Skill
|
|
6
|
+
|
|
7
|
+
Skill id:
|
|
8
|
+
|
|
9
|
+
- `lazyedit-publish-workflow`
|
|
10
|
+
|
|
11
|
+
Source skill:
|
|
12
|
+
|
|
13
|
+
- `/home/lachlan/.codex/skills/lazyedit-publish-workflow/SKILL.md`
|
|
14
|
+
|
|
15
|
+
AgInTiFlow project-local copy:
|
|
16
|
+
|
|
17
|
+
- `/home/lachlan/ProjectsLFS/Agent/AgInTiFlow/.aginti/skills/lazyedit-publish-workflow/SKILL.md`
|
|
18
|
+
|
|
19
|
+
Global AgInTiFlow custom copy:
|
|
20
|
+
|
|
21
|
+
- Skill Mesh storage under `/home/lachlan/.agintiflow/skillmesh/skills/lazyedit-publish-workflow/`
|
|
22
|
+
- Installed as a local-reviewed enabled Skill Mesh skill.
|
|
23
|
+
|
|
24
|
+
## Why Both Copies Exist
|
|
25
|
+
|
|
26
|
+
The project-local `.aginti/skills/` copy documents the skill in the AgInTiFlow repository and lets local development sessions load it as `source=project-local`.
|
|
27
|
+
|
|
28
|
+
The Skill Mesh copy makes the same skill available from other working directories such as LALACHAN, RARACHAN, LazyEdit, or Agent without requiring the user to start AgInTiFlow from the AgInTiFlow repo.
|
|
29
|
+
|
|
30
|
+
## Verification
|
|
31
|
+
|
|
32
|
+
Project-local loader:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
node -e "import { listSkills, selectSkillsForGoal } from './src/skill-library.js'; const skill=listSkills({projectRoot:process.cwd(), includeBody:true}).find(s=>s.id==='lazyedit-publish-workflow'); if(!skill) throw new Error('missing'); console.log(skill.source);"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Expected result:
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
project-local
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Global Skill Mesh install:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
node bin/aginti-cli.js skillmesh export lazyedit-publish-workflow --out /tmp/lazyedit-publish-workflow.skillpack.json
|
|
48
|
+
node bin/aginti-cli.js skillmesh import /tmp/lazyedit-publish-workflow.skillpack.json --enable
|
|
49
|
+
node bin/aginti-cli.js skillmesh status
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Expected result includes:
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
enabled lazyedit-publish-workflow: LazyEdit Publish Workflow
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Cross-directory discovery:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
cd /home/lachlan/ProjectsLFS/Agent
|
|
62
|
+
aginti skills lazyedit
|
|
63
|
+
|
|
64
|
+
cd /home/lachlan/ProjectsLFS/LALACHAN
|
|
65
|
+
node /home/lachlan/ProjectsLFS/Agent/AgInTiFlow/bin/aginti-cli.js skills lazyedit
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Expected result:
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
lazyedit-publish-workflow ... source=skillmesh
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Selection check:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
node -e "import { selectSkillsForGoal } from '/home/lachlan/ProjectsLFS/Agent/AgInTiFlow/src/skill-library.js'; const selected=selectSkillsForGoal('publish latest LALACHAN generated video through LazyEdit to Shipinhao YouTube Instagram with corrected subtitles', {projectRoot:'/home/lachlan/ProjectsLFS/LALACHAN', includeBody:true}).map(s=>s.id); console.log(selected); if(!selected.includes('lazyedit-publish-workflow')) throw new Error('not selected');"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Expected result:
|
|
81
|
+
|
|
82
|
+
- `lazyedit-publish-workflow` appears in the selected skill list.
|
|
83
|
+
|
|
84
|
+
## Operational Notes
|
|
85
|
+
|
|
86
|
+
The skill is prompt guidance, not a privileged tool. LazyEdit publishing still runs through normal AgInTiFlow permission policy and shell/tmux safeguards.
|
|
87
|
+
|
|
88
|
+
The subtitle correction rule is preserved: generated video scripts/stories are reference context for correcting ASR recognition errors, not verbatim targets. The agent should compare ASR output against the script, infer intended wording, preserve timing/line structure, and avoid blindly copying script text when the audio differs.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Paper Revision Skill Installation
|
|
2
|
+
|
|
3
|
+
Date: 2026-06-03
|
|
4
|
+
|
|
5
|
+
Source repository:
|
|
6
|
+
|
|
7
|
+
- `/home/lachlan/ProjectsLFS/paper-revision-skill`
|
|
8
|
+
- `https://github.com/lachlanchen/paper-revision-skill`
|
|
9
|
+
|
|
10
|
+
Purpose:
|
|
11
|
+
|
|
12
|
+
- Add plan-gated academic manuscript revision as a reusable skill.
|
|
13
|
+
- Keep manuscript-specific revision policy in the skill, not in AgInTiFlow core.
|
|
14
|
+
- Let Codex, AgInTiFlow, Claude, Gemini, and Copilot consume the same workflow guidance with tool-specific entry files.
|
|
15
|
+
|
|
16
|
+
AgInTiFlow integration:
|
|
17
|
+
|
|
18
|
+
- The repository's root `SKILL.md` is Codex-style and uses `name:` plus `description:`.
|
|
19
|
+
- AgInTiFlow SkillMesh now accepts that dialect by normalizing `name:` to native `id:` and deriving `label:` when missing.
|
|
20
|
+
- Install the root `SKILL.md` as a reviewed local SkillMesh skill and enable it.
|
|
21
|
+
|
|
22
|
+
Suggested install command from the AgInTiFlow repository:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
node --input-type=module - <<'NODE'
|
|
26
|
+
import fs from "node:fs/promises";
|
|
27
|
+
import {
|
|
28
|
+
buildSkillPackFromMarkdown,
|
|
29
|
+
enableSkillMeshSkill,
|
|
30
|
+
installSkillPack,
|
|
31
|
+
} from "./src/skillmesh.js";
|
|
32
|
+
|
|
33
|
+
const content = await fs.readFile("/home/lachlan/ProjectsLFS/paper-revision-skill/SKILL.md", "utf8");
|
|
34
|
+
const pack = await buildSkillPackFromMarkdown(content, { valueScore: 95 });
|
|
35
|
+
await installSkillPack(pack, { enabled: true });
|
|
36
|
+
await enableSkillMeshSkill("paper-revision-skill", true);
|
|
37
|
+
console.log(JSON.stringify({ ok: true, installedSkills: pack.skills.map((skill) => skill.id), packHash: pack.packHash }, null, 2));
|
|
38
|
+
NODE
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Validation:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
node scripts/smoke-skillmesh.js
|
|
45
|
+
node --input-type=module - <<'NODE'
|
|
46
|
+
import { selectSkillsForGoal } from "./src/skill-library.js";
|
|
47
|
+
const selected = selectSkillsForGoal(
|
|
48
|
+
"Use the paper revision workflow to answer reviewer comments before editing LaTeX.",
|
|
49
|
+
{ limit: 8, includeBody: false }
|
|
50
|
+
);
|
|
51
|
+
console.log(JSON.stringify(selected.map((skill) => ({ id: skill.id, source: skill.source })), null, 2));
|
|
52
|
+
if (!selected.some((skill) => skill.id === "paper-revision-skill")) {
|
|
53
|
+
throw new Error("paper-revision-skill was not selected");
|
|
54
|
+
}
|
|
55
|
+
NODE
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Boundary:
|
|
59
|
+
|
|
60
|
+
- Do not move manuscript-revision details into AgInTiFlow core.
|
|
61
|
+
- Improve AgInTiFlow core only when the issue is general skill compatibility, skill discovery, SkillMesh validation, evidence tracking, PDF/build tooling, or tool invocation reliability.
|
|
62
|
+
- Keep the skill's plan-first, edit-second, PDF-verified revision workflow in the skill repository.
|
|
@@ -223,7 +223,7 @@ if (!keyStatus.deepseek) {
|
|
|
223
223
|
{
|
|
224
224
|
ok: false,
|
|
225
225
|
skipped: true,
|
|
226
|
-
reason: "DeepSeek key is not available by env or project-local .aginti/.env.",
|
|
226
|
+
reason: "DeepSeek key is not available by env, account ~/.agintiflow/.env, or project-local .aginti/.env.",
|
|
227
227
|
},
|
|
228
228
|
null,
|
|
229
229
|
2
|
package/scripts/smoke-auth.js
CHANGED
|
@@ -21,7 +21,9 @@ const envKeys = [
|
|
|
21
21
|
"GRSAI_API_KEY",
|
|
22
22
|
];
|
|
23
23
|
const originalEnv = Object.fromEntries(envKeys.map((key) => [key, process.env[key]]));
|
|
24
|
+
const originalAgintiflowHome = process.env.AGINTIFLOW_HOME;
|
|
24
25
|
for (const key of envKeys) delete process.env[key];
|
|
26
|
+
process.env.AGINTIFLOW_HOME = path.join(tempRoot, ".agintiflow-home");
|
|
25
27
|
|
|
26
28
|
function assert(condition, message) {
|
|
27
29
|
if (!condition) throw new Error(message);
|
|
@@ -85,18 +87,32 @@ try {
|
|
|
85
87
|
const veniceDefaults = getProviderDefaults("venice");
|
|
86
88
|
assert(veniceDefaults.provider === "venice" && veniceDefaults.model === "venice-uncensored-1-2", "venice provider defaults are not available");
|
|
87
89
|
|
|
90
|
+
const projectOnlyRoot = path.join(tempRoot, "project-only");
|
|
91
|
+
await fs.mkdir(path.join(projectOnlyRoot, ".aginti"), { recursive: true });
|
|
92
|
+
await fs.writeFile(path.join(projectOnlyRoot, ".aginti", ".env"), 'OPENAI_API_KEY="project-only-openai-key"\n', "utf8");
|
|
93
|
+
let projectOnlyStatus = providerKeyStatus(projectOnlyRoot);
|
|
94
|
+
assert(projectOnlyStatus.openai && projectOnlyStatus.projectEnv, "project-only key was not loaded");
|
|
95
|
+
await fs.access(path.join(tempRoot, ".agintiflow-home", ".env"))
|
|
96
|
+
.then(() => {
|
|
97
|
+
throw new Error("project-only key was incorrectly promoted into account-wide env");
|
|
98
|
+
})
|
|
99
|
+
.catch((error) => {
|
|
100
|
+
if (error?.code !== "ENOENT") throw error;
|
|
101
|
+
});
|
|
102
|
+
delete process.env.OPENAI_API_KEY;
|
|
103
|
+
|
|
88
104
|
process.env.DEEPSEEK_API_KEY = "ambient-deepseek-key";
|
|
89
105
|
process.env.OPENROUTER_API_KEY = "ambient-openrouter-key";
|
|
90
106
|
process.env.VENICE_API_KEY = "ambient-venice-key";
|
|
91
107
|
process.env.GRSAI_API_KEY = "ambient-grsai-key";
|
|
92
108
|
let status = providerKeyStatus(tempRoot);
|
|
93
109
|
assert(status.deepseek && status.openrouter && status.venice && status.grsai, "ambient provider keys were not detected");
|
|
94
|
-
assert(status.
|
|
95
|
-
const
|
|
96
|
-
assert(
|
|
97
|
-
assert(
|
|
98
|
-
assert(
|
|
99
|
-
assert(
|
|
110
|
+
assert(status.globalEnv, "ambient provider keys were not persisted into account-wide ~/.agintiflow/.env");
|
|
111
|
+
const globalEnv = await fs.readFile(path.join(tempRoot, ".agintiflow-home", ".env"), "utf8");
|
|
112
|
+
assert(globalEnv.includes("DEEPSEEK_API_KEY="), "ambient DeepSeek key was not saved globally");
|
|
113
|
+
assert(globalEnv.includes("OPENROUTER_API_KEY="), "ambient OpenRouter key was not saved globally");
|
|
114
|
+
assert(globalEnv.includes("VENICE_API_KEY="), "ambient Venice key was not saved globally");
|
|
115
|
+
assert(globalEnv.includes("GRSAI_API_KEY="), "ambient GRS AI key was not saved globally");
|
|
100
116
|
delete process.env.DEEPSEEK_API_KEY;
|
|
101
117
|
delete process.env.OPENROUTER_API_KEY;
|
|
102
118
|
delete process.env.VENICE_API_KEY;
|
|
@@ -126,8 +142,12 @@ try {
|
|
|
126
142
|
|
|
127
143
|
await runCli(["keys", "set", "openai", "--stdin"], "test-openai-key");
|
|
128
144
|
await runCli(["keys", "set", "grsai", "--stdin"], "test-grsai-key");
|
|
145
|
+
await runCli(["keys", "set", "--project", "deepseek", "--stdin"], "project-deepseek-key");
|
|
129
146
|
status = providerKeyStatus(tempRoot);
|
|
130
|
-
assert(status.openai && status.openrouter && status.grsai && status.qwen && status.venice, "stored auth keys were not detected");
|
|
147
|
+
assert(status.openai && status.openrouter && status.grsai && status.qwen && status.venice && status.deepseek, "stored auth keys were not detected");
|
|
148
|
+
assert(status.projectEnv, "project-scoped key was not recorded as project env");
|
|
149
|
+
const projectEnv = await fs.readFile(path.join(tempRoot, ".aginti", ".env"), "utf8");
|
|
150
|
+
assert(projectEnv.includes("DEEPSEEK_API_KEY="), "project-scoped DeepSeek key was not saved in .aginti/.env");
|
|
131
151
|
const openaiPreview = providerKeyPreview(tempRoot, "openai");
|
|
132
152
|
assert(openaiPreview.preview === "test…-key (15 chars)", "openai key preview was not masked correctly");
|
|
133
153
|
assert(openaiPreview.preview !== "test-openai-key", "openai key preview leaked raw key");
|
|
@@ -136,6 +156,8 @@ try {
|
|
|
136
156
|
assert(keysOutput.includes("openrouter=available"), "keys status did not include openrouter");
|
|
137
157
|
assert(keysOutput.includes("qwen=available"), "keys status did not include qwen");
|
|
138
158
|
assert(keysOutput.includes("venice=available"), "keys status did not include venice");
|
|
159
|
+
assert(keysOutput.includes("globalEnv=yes"), "keys status did not show global env");
|
|
160
|
+
assert(keysOutput.includes("projectEnv=yes"), "keys status did not show project env");
|
|
139
161
|
assert(
|
|
140
162
|
!keysOutput.includes("test-openai-key") &&
|
|
141
163
|
!keysOutput.includes("test-openrouter-key") &&
|
|
@@ -157,7 +179,9 @@ try {
|
|
|
157
179
|
"qwen-defaults",
|
|
158
180
|
"openrouter-defaults",
|
|
159
181
|
"venice-defaults",
|
|
160
|
-
"
|
|
182
|
+
"project-key-not-auto-promoted",
|
|
183
|
+
"ambient-key-global-autopersist",
|
|
184
|
+
"project-key-override",
|
|
161
185
|
"qwen-key-status",
|
|
162
186
|
"openrouter-key-status",
|
|
163
187
|
"venice-key-status",
|
|
@@ -173,5 +197,7 @@ try {
|
|
|
173
197
|
if (originalEnv[key] === undefined) delete process.env[key];
|
|
174
198
|
else process.env[key] = originalEnv[key];
|
|
175
199
|
}
|
|
200
|
+
if (originalAgintiflowHome === undefined) delete process.env.AGINTIFLOW_HOME;
|
|
201
|
+
else process.env.AGINTIFLOW_HOME = originalAgintiflowHome;
|
|
176
202
|
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
177
203
|
}
|
|
@@ -14,7 +14,18 @@ import { SessionStore } from "../src/session-store.js";
|
|
|
14
14
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
15
15
|
const execFile = promisify(execFileCallback);
|
|
16
16
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-auxiliary-"));
|
|
17
|
+
const originalImageEnv = {
|
|
18
|
+
AGINTIFLOW_HOME: process.env.AGINTIFLOW_HOME,
|
|
19
|
+
GRSAI: process.env.GRSAI,
|
|
20
|
+
GRSAI_API_KEY: process.env.GRSAI_API_KEY,
|
|
21
|
+
VENICE_API_KEY: process.env.VENICE_API_KEY,
|
|
22
|
+
AGINTI_AUX_PROVIDER: process.env.AGINTI_AUX_PROVIDER,
|
|
23
|
+
};
|
|
17
24
|
process.env.AGINTIFLOW_HOME = path.join(tempRoot, ".agintiflow-home");
|
|
25
|
+
delete process.env.GRSAI;
|
|
26
|
+
delete process.env.GRSAI_API_KEY;
|
|
27
|
+
delete process.env.VENICE_API_KEY;
|
|
28
|
+
delete process.env.AGINTI_AUX_PROVIDER;
|
|
18
29
|
const runtimeDir = path.join(tempRoot, "runtime");
|
|
19
30
|
const workspace = path.join(tempRoot, "workspace");
|
|
20
31
|
await fs.mkdir(workspace, { recursive: true });
|
|
@@ -24,8 +35,21 @@ function assert(condition, message) {
|
|
|
24
35
|
}
|
|
25
36
|
|
|
26
37
|
try {
|
|
27
|
-
await setProviderKey(workspace, "grsai", "test-grsai-key");
|
|
28
38
|
await setProviderKey(workspace, "venice", "test-venice-key");
|
|
39
|
+
const autoVeniceDryRun = await generateImage(
|
|
40
|
+
{
|
|
41
|
+
prompt: "A small cyan robot holding a paintbrush, clean bright product illustration.",
|
|
42
|
+
outputDir: "artifacts/images/auto-venice-dry-run",
|
|
43
|
+
outputStem: "robot",
|
|
44
|
+
dryRun: true,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
commandCwd: workspace,
|
|
48
|
+
allowFileTools: true,
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
assert(autoVeniceDryRun.ok && autoVeniceDryRun.provider === "venice", "generate_image did not auto-select Venice when only Venice image key was available");
|
|
52
|
+
await setProviderKey(workspace, "grsai", "test-grsai-key");
|
|
29
53
|
const keyStatus = providerKeyStatus(workspace);
|
|
30
54
|
assert(keyStatus.grsai, "GRSAI key status was not detected");
|
|
31
55
|
assert(keyStatus.venice, "Venice key status was not detected");
|
|
@@ -163,6 +187,7 @@ try {
|
|
|
163
187
|
"image_skill_listed",
|
|
164
188
|
"venice_image_skill_listed",
|
|
165
189
|
"generate_image_dry_run",
|
|
190
|
+
"auto_venice_when_grsai_missing",
|
|
166
191
|
"venice_generate_image_dry_run",
|
|
167
192
|
"svg_request_png_fallback",
|
|
168
193
|
"direct_image_cli_svg_request_png_fallback",
|
|
@@ -175,5 +200,9 @@ try {
|
|
|
175
200
|
)
|
|
176
201
|
);
|
|
177
202
|
} finally {
|
|
203
|
+
for (const [key, value] of Object.entries(originalImageEnv)) {
|
|
204
|
+
if (value === undefined) delete process.env[key];
|
|
205
|
+
else process.env[key] = value;
|
|
206
|
+
}
|
|
178
207
|
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
179
208
|
}
|
|
@@ -64,6 +64,28 @@ assert(!listSkills({ includeBody: false }).some((skill) => skill.id === "mesh-an
|
|
|
64
64
|
await setSkillMeshMode("share");
|
|
65
65
|
const config = await loadSkillMeshConfig();
|
|
66
66
|
assert(config.mode === "share", "share mode should persist");
|
|
67
|
+
|
|
68
|
+
const codexStyleMarkdown = [
|
|
69
|
+
"---",
|
|
70
|
+
"name: paper-revision-skill",
|
|
71
|
+
"description: Plan-gated academic manuscript revision workflow for LaTeX papers, reviewer responses, proof corrections, redlines, PDF-backed audits, and submission packaging.",
|
|
72
|
+
"---",
|
|
73
|
+
"",
|
|
74
|
+
"Plan first, edit second, verify from compiled PDFs, and keep revision evidence under references/.",
|
|
75
|
+
"",
|
|
76
|
+
].join("\n");
|
|
77
|
+
const codexPack = await buildSkillPackFromMarkdown(codexStyleMarkdown, { valueScore: 90 });
|
|
78
|
+
assert(codexPack.skills[0].id === "paper-revision-skill", "Codex-style name should become SkillMesh id");
|
|
79
|
+
assert(codexPack.skills[0].content.includes("id: paper-revision-skill"), "Codex-style skill should be normalized with id");
|
|
80
|
+
assert(codexPack.skills[0].content.includes("label: Paper Revision Skill"), "Codex-style skill should receive a label");
|
|
81
|
+
await installSkillPack(codexPack, { enabled: true });
|
|
82
|
+
assert(
|
|
83
|
+
selectSkillsForGoal("use the paper revision workflow to answer reviewer comments before editing latex", { limit: 8 }).some(
|
|
84
|
+
(skill) => skill.id === "paper-revision-skill"
|
|
85
|
+
),
|
|
86
|
+
"Codex-style imported skill should be selectable"
|
|
87
|
+
);
|
|
88
|
+
|
|
67
89
|
const serviceUnit = buildSkillMeshServiceUnit({
|
|
68
90
|
dataDir: path.join(tempRoot, "relay-service"),
|
|
69
91
|
runScript: path.join(tempRoot, "relay-service", "aginti-skill-relay.run.sh"),
|
|
@@ -120,6 +142,7 @@ console.log(
|
|
|
120
142
|
"metadata-sync",
|
|
121
143
|
"soft-unreachable-node",
|
|
122
144
|
"systemd-service-unit",
|
|
145
|
+
"codex-style-skill-import",
|
|
123
146
|
],
|
|
124
147
|
},
|
|
125
148
|
null,
|
package/src/auth-onboarding.js
CHANGED
|
@@ -420,7 +420,7 @@ export async function promptAndSaveDeepSeekKey(projectRoot = process.cwd(), opti
|
|
|
420
420
|
);
|
|
421
421
|
if (!key) return { saved: false, skipped: true };
|
|
422
422
|
|
|
423
|
-
const result = await setProviderKey(projectRoot, "deepseek", key);
|
|
423
|
+
const result = await setProviderKey(projectRoot, "deepseek", key, { scope: options.scope || "global" });
|
|
424
424
|
return {
|
|
425
425
|
saved: true,
|
|
426
426
|
provider: result.provider,
|
|
@@ -431,6 +431,8 @@ export async function promptAndSaveDeepSeekKey(projectRoot = process.cwd(), opti
|
|
|
431
431
|
|
|
432
432
|
export async function runAuthWizard(projectRoot = process.cwd(), options = {}) {
|
|
433
433
|
const status = providerKeyStatus(projectRoot);
|
|
434
|
+
const scope = ["project", "local"].includes(String(options.scope || "").toLowerCase()) ? "project" : "global";
|
|
435
|
+
const scopeLabel = scope === "project" ? "project .aginti/.env" : "account ~/.agintiflow/.env";
|
|
434
436
|
const initialProvider = normalizeAuthProvider(options.provider || options.initialProvider || "deepseek", "deepseek");
|
|
435
437
|
const directProvider =
|
|
436
438
|
options.provider && ["deepseek", "openai", "openrouter", "qwen", "venice", "grsai"].includes(normalizeAuthProvider(options.provider, ""))
|
|
@@ -454,7 +456,7 @@ export async function runAuthWizard(projectRoot = process.cwd(), options = {}) {
|
|
|
454
456
|
const current = preview.available ? `available: ${preview.preview}` : "missing";
|
|
455
457
|
const keyHelp = authProviderKeyHelp(mainProvider);
|
|
456
458
|
const title = `${providerLabel(mainProvider)} main API key`;
|
|
457
|
-
const prompt = `${keyHelp ? `${keyHelp}\n` : ""}${title} (${current}) [hidden]: `;
|
|
459
|
+
const prompt = `${keyHelp ? `${keyHelp}\n` : ""}${title} (${current}) [hidden, saves to ${scopeLabel}]: `;
|
|
458
460
|
const secret = await promptSecret(prompt, {
|
|
459
461
|
allowEscape: true,
|
|
460
462
|
box: {
|
|
@@ -463,12 +465,12 @@ export async function runAuthWizard(projectRoot = process.cwd(), options = {}) {
|
|
|
463
465
|
statusText: preview.available ? `Existing ${preview.keyName}: ${preview.preview}` : `Missing ${preview.keyName || "provider key"}`,
|
|
464
466
|
currentPreview: preview.preview,
|
|
465
467
|
actionText: preview.available
|
|
466
|
-
?
|
|
467
|
-
:
|
|
468
|
+
? `Existing key is selected. Type to replace in ${scopeLabel}, Enter/Esc to keep existing.`
|
|
469
|
+
: `Paste key and Enter to save to ${scopeLabel}, or Esc/Enter to skip.`,
|
|
468
470
|
},
|
|
469
471
|
});
|
|
470
472
|
if (secret.value) {
|
|
471
|
-
const result = await setProviderKey(projectRoot, mainProvider, secret.value);
|
|
473
|
+
const result = await setProviderKey(projectRoot, mainProvider, secret.value, { scope });
|
|
472
474
|
saved.push(result);
|
|
473
475
|
} else {
|
|
474
476
|
skipped.push({ provider: mainProvider, reason: secret.skipped ? "skipped" : "empty" });
|
|
@@ -481,18 +483,18 @@ export async function runAuthWizard(projectRoot = process.cwd(), options = {}) {
|
|
|
481
483
|
const preview = providerKeyPreview(projectRoot, "grsai");
|
|
482
484
|
const current = preview.available ? `available: ${preview.preview}` : "optional";
|
|
483
485
|
const title = `${AUXILIARY_AUTH_PROVIDER.label} auxiliary image key`;
|
|
484
|
-
const secret = await promptSecret(`${title} (${current}) [hidden]: `, {
|
|
486
|
+
const secret = await promptSecret(`${title} (${current}) [hidden, saves to ${scopeLabel}]: `, {
|
|
485
487
|
allowEscape: true,
|
|
486
488
|
box: {
|
|
487
489
|
title,
|
|
488
490
|
helpText: "Optional image generation key for GRS AI / Nano Banana.",
|
|
489
491
|
statusText: preview.available ? `Existing ${preview.keyName}: ${preview.preview}` : "Optional; no auxiliary key saved.",
|
|
490
492
|
currentPreview: preview.preview,
|
|
491
|
-
actionText: preview.available ?
|
|
493
|
+
actionText: preview.available ? `Existing key is selected. Type to replace in ${scopeLabel}, Enter/Esc to keep existing.` : `Paste key and Enter to save to ${scopeLabel}, or Esc/Enter to skip.`,
|
|
492
494
|
},
|
|
493
495
|
});
|
|
494
496
|
if (secret.value) {
|
|
495
|
-
const result = await setProviderKey(projectRoot, "grsai", secret.value);
|
|
497
|
+
const result = await setProviderKey(projectRoot, "grsai", secret.value, { scope });
|
|
496
498
|
saved.push(result);
|
|
497
499
|
} else {
|
|
498
500
|
skipped.push({ provider: "grsai", reason: "skipped" });
|
|
@@ -500,17 +502,17 @@ export async function runAuthWizard(projectRoot = process.cwd(), options = {}) {
|
|
|
500
502
|
} else if (directProvider === "grsai") {
|
|
501
503
|
const preview = providerKeyPreview(projectRoot, "grsai");
|
|
502
504
|
const title = `${AUXILIARY_AUTH_PROVIDER.label} auxiliary image key`;
|
|
503
|
-
const secret = await promptSecret(`${title} [hidden]: `, {
|
|
505
|
+
const secret = await promptSecret(`${title} [hidden, saves to ${scopeLabel}]: `, {
|
|
504
506
|
allowEscape: true,
|
|
505
507
|
box: {
|
|
506
508
|
title,
|
|
507
509
|
helpText: "Optional image generation key for GRS AI / Nano Banana.",
|
|
508
510
|
statusText: preview.available ? `Existing ${preview.keyName}: ${preview.preview}` : "Missing auxiliary key.",
|
|
509
511
|
currentPreview: preview.preview,
|
|
510
|
-
actionText: preview.available ?
|
|
512
|
+
actionText: preview.available ? `Existing key is selected. Type to replace in ${scopeLabel}, Enter/Esc to keep existing.` : `Paste key and Enter to save to ${scopeLabel}, or Esc/Enter to skip.`,
|
|
511
513
|
},
|
|
512
514
|
});
|
|
513
|
-
if (secret.value) saved.push(await setProviderKey(projectRoot, "grsai", secret.value));
|
|
515
|
+
if (secret.value) saved.push(await setProviderKey(projectRoot, "grsai", secret.value, { scope }));
|
|
514
516
|
else skipped.push({ provider: "grsai", reason: "skipped" });
|
|
515
517
|
}
|
|
516
518
|
|
package/src/auxiliary-tools.js
CHANGED
|
@@ -324,14 +324,28 @@ function resultUrls(payload) {
|
|
|
324
324
|
|
|
325
325
|
function normalizeImageProvider(value = "") {
|
|
326
326
|
const normalized = String(value || "").trim().toLowerCase();
|
|
327
|
+
if (!normalized || ["auto", "default"].includes(normalized)) return "";
|
|
327
328
|
if (["venice", "venice-image", "venice-ai", "veniceai"].includes(normalized)) return "venice";
|
|
329
|
+
if (["grsai", "grs", "nano-banana", "nanobanana"].includes(normalized)) return "grsai";
|
|
330
|
+
if (["openai", "deepseek", "qwen", "openrouter", "mock"].includes(normalized)) {
|
|
331
|
+
throw new Error(
|
|
332
|
+
`Unsupported image provider "${normalized}". generate_image is raster-only through GRS AI or Venice image keys; text-model keys for ${normalized} do not enable image generation.`
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
throw new Error("Unsupported image provider. Use grsai, venice, or auto.");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function autoImageProvider() {
|
|
339
|
+
if (grsaiKey()) return "grsai";
|
|
340
|
+
if (veniceKey()) return "venice";
|
|
328
341
|
return "grsai";
|
|
329
342
|
}
|
|
330
343
|
|
|
331
344
|
function defaultImageProvider(config = {}) {
|
|
332
|
-
|
|
345
|
+
const explicit = normalizeImageProvider(
|
|
333
346
|
config.auxiliaryProvider || process.env.AGINTI_AUX_PROVIDER || process.env.VENICE_IMAGE_PROVIDER || process.env.GRSAI_IMAGE_PROVIDER || ""
|
|
334
347
|
);
|
|
348
|
+
return explicit || autoImageProvider();
|
|
335
349
|
}
|
|
336
350
|
|
|
337
351
|
function veniceBaseUrl(value = "") {
|
|
@@ -343,7 +357,7 @@ function veniceBaseUrl(value = "") {
|
|
|
343
357
|
async function generateVeniceImages({ prompt, args, target, outputStem, manifest, manifestPath, formatInfo, signal = null }) {
|
|
344
358
|
const apiKey = veniceKey();
|
|
345
359
|
if (!apiKey) {
|
|
346
|
-
throw new Error("Missing VENICE_API_KEY. Run `aginti login venice` or `aginti keys set venice --stdin
|
|
360
|
+
throw new Error("Missing VENICE_API_KEY for image generation. Run `aginti login venice` or `aginti keys set venice --stdin`; text-model keys do not enable raster image generation.");
|
|
347
361
|
}
|
|
348
362
|
|
|
349
363
|
const model =
|
|
@@ -511,7 +525,19 @@ export async function generateImage(args = {}, config = {}) {
|
|
|
511
525
|
|
|
512
526
|
const apiKey = grsaiKey();
|
|
513
527
|
if (!apiKey) {
|
|
514
|
-
|
|
528
|
+
if (veniceKey()) {
|
|
529
|
+
return generateVeniceImages({ prompt, args, target, outputStem, manifest, manifestPath, formatInfo, signal: config.abortSignal });
|
|
530
|
+
}
|
|
531
|
+
const textKeys = [
|
|
532
|
+
process.env.OPENAI_API_KEY || process.env.LLM_API_KEY ? "OpenAI/LLM" : "",
|
|
533
|
+
process.env.DEEPSEEK_API_KEY ? "DeepSeek" : "",
|
|
534
|
+
process.env.OPENROUTER_API_KEY ? "OpenRouter" : "",
|
|
535
|
+
process.env.QWEN_API_KEY ? "Qwen" : "",
|
|
536
|
+
].filter(Boolean);
|
|
537
|
+
const suffix = textKeys.length
|
|
538
|
+
? ` Text-model keys are available (${textKeys.join(", ")}), but raster image generation requires GRSAI or VENICE_API_KEY.`
|
|
539
|
+
: "";
|
|
540
|
+
throw new Error(`Missing image-generation key. Run \`aginti login grsai\`, \`aginti login venice\`, \`aginti keys set grsai --stdin\`, or \`/auxiliary grsai\`.${suffix}`);
|
|
515
541
|
}
|
|
516
542
|
|
|
517
543
|
const submitUrl = `${host}/v1/draw/nano-banana`;
|
package/src/capabilities.js
CHANGED
|
@@ -245,6 +245,8 @@ export async function buildCapabilityReport(projectRoot, packageVersion, config)
|
|
|
245
245
|
grsai: keyStatus.grsai,
|
|
246
246
|
mock: true,
|
|
247
247
|
localEnv: keyStatus.localEnv,
|
|
248
|
+
globalEnv: keyStatus.globalEnv,
|
|
249
|
+
projectEnv: keyStatus.projectEnv,
|
|
248
250
|
envVars: keyStatus.envVars,
|
|
249
251
|
},
|
|
250
252
|
tools: {
|
|
@@ -327,7 +329,7 @@ export function printCapabilityReport(report) {
|
|
|
327
329
|
report.keys.venice ? "available" : "missing"
|
|
328
330
|
} grsai=${
|
|
329
331
|
report.keys.grsai ? "available" : "missing"
|
|
330
|
-
} mock=available
|
|
332
|
+
} mock=available globalEnv=${report.keys.globalEnv ? "yes" : "no"} projectEnv=${report.keys.projectEnv ? "yes" : "no"}`
|
|
331
333
|
);
|
|
332
334
|
for (const check of report.checks) {
|
|
333
335
|
const suffix = check.version ? ` ${check.version}` : check.reason ? ` ${check.reason}` : check.hint ? ` ${check.hint}` : "";
|
package/src/cli.js
CHANGED
|
@@ -776,7 +776,7 @@ function exitOnUnknownOptions(parsed) {
|
|
|
776
776
|
|
|
777
777
|
function printUsage() {
|
|
778
778
|
console.log(
|
|
779
|
-
'Usage: aginti [chat] OR aginti init [--template minimal|disciplined|coding|research|writing|design|aaps|supervision] OR aginti web [--port 3210] OR aginti docker [status|setup|install-host] OR aginti update OR aginti image [--json] [--dry-run] [--format png|webp|svg] "prompt" OR aginti models OR aginti aaps [status|init|files|validate|compile|check|run] OR aginti mcp [status|config|inspect|tools|resources|read|prompts|prompt|call|restart] OR aginti skills [query] OR aginti skillmesh [status|off|record|share|sync|serve|service] OR aginti housekeeping [--json] OR aginti auth [deepseek|openai|openrouter|qwen|venice|grsai] OR aginti resume [--all-sessions] [latest|<session-id>] ["prompt"] OR aginti --remove-empty-sessions OR aginti --remove-sessions OR aginti queue <session-id> "message" OR aginti [--no-auto-update] [-s safe|normal|danger] [--language en|ja|zh-Hans|zh-Hant|ko|fr|es|ar|vi|de|ru] [--image] [--latex] [--scs|--scs auto|--no-scs] [--dynamic-steps auto|on|off] [--routing smart|fast|complex|manual] [--provider deepseek|openai|openrouter|qwen|venice|mock] [--model MODEL] [--route-model MODEL --route-reasoning provider-default|minimal|low|medium|high|xhigh] [--main-model MODEL --main-reasoning provider-default|minimal|low|medium|high|xhigh] [--spare-model MODEL --spare-reasoning medium] [--aux-provider grsai|venice --aux-model MODEL] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--approve-package-installs] [--allow-shell|--no-shell] [--allow-file-tools|--no-file-tools] [--web-search|--no-web-search] [--mcp|--no-mcp] [--parallel-scouts|--no-parallel-scouts --scout-count 1..10] [--allow-auxiliary-tools|--no-auxiliary-tools] [--allow-wrappers --wrapper codex --wrapper-model gpt-5.5] [--list-models|--list-routes] "your task"'
|
|
779
|
+
'Usage: aginti [chat] OR aginti init [--template minimal|disciplined|coding|research|writing|design|aaps|supervision] OR aginti web [--port 3210] OR aginti docker [status|setup|install-host] OR aginti update OR aginti image [--json] [--dry-run] [--format png|webp|svg] "prompt" OR aginti models OR aginti aaps [status|init|files|validate|compile|check|run] OR aginti mcp [status|config|inspect|tools|resources|read|prompts|prompt|call|restart] OR aginti skills [query] OR aginti skillmesh [status|off|record|share|sync|serve|service] OR aginti housekeeping [--json] OR aginti auth [--project] [deepseek|openai|openrouter|qwen|venice|grsai] OR aginti resume [--all-sessions] [latest|<session-id>] ["prompt"] OR aginti --remove-empty-sessions OR aginti --remove-sessions OR aginti queue <session-id> "message" OR aginti [--no-auto-update] [-s safe|normal|danger] [--language en|ja|zh-Hans|zh-Hant|ko|fr|es|ar|vi|de|ru] [--image] [--latex] [--scs|--scs auto|--no-scs] [--dynamic-steps auto|on|off] [--routing smart|fast|complex|manual] [--provider deepseek|openai|openrouter|qwen|venice|mock] [--model MODEL] [--route-model MODEL --route-reasoning provider-default|minimal|low|medium|high|xhigh] [--main-model MODEL --main-reasoning provider-default|minimal|low|medium|high|xhigh] [--spare-model MODEL --spare-reasoning medium] [--aux-provider grsai|venice --aux-model MODEL] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--approve-package-installs] [--allow-shell|--no-shell] [--allow-file-tools|--no-file-tools] [--web-search|--no-web-search] [--mcp|--no-mcp] [--parallel-scouts|--no-parallel-scouts --scout-count 1..10] [--allow-auxiliary-tools|--no-auxiliary-tools] [--allow-wrappers --wrapper codex --wrapper-model gpt-5.5] [--list-models|--list-routes] "your task"'
|
|
780
780
|
);
|
|
781
781
|
console.log("Permission shortcuts: -s safe asks before writes/setup; -s normal allows current-project writes and Docker setup; -s danger enables trusted host/full-access mode.");
|
|
782
782
|
console.log(`Languages: ${["en", "ja", "zh-Hans", "zh-Hant", "ko", "fr", "es", "ar", "vi", "de", "ru"].map((code) => `${code}=${languageLabel(code)}`).join(", ")}`);
|
|
@@ -1382,7 +1382,7 @@ function printDoctorReport(report) {
|
|
|
1382
1382
|
report.keys.venice ? "available" : "missing"
|
|
1383
1383
|
} grsai=${
|
|
1384
1384
|
report.keys.grsai ? "available" : "missing"
|
|
1385
|
-
} mock=available
|
|
1385
|
+
} mock=available globalEnv=${report.keys.globalEnv ? "yes" : "no"} projectEnv=${report.keys.projectEnv ? "yes" : "no"}`
|
|
1386
1386
|
);
|
|
1387
1387
|
console.log(
|
|
1388
1388
|
`sandbox=${report.sandbox?.sandboxMode || "unknown"} docker=${
|
|
@@ -1412,8 +1412,8 @@ async function readStdin() {
|
|
|
1412
1412
|
|
|
1413
1413
|
async function ensureDeepSeekKeyForOneShot(args) {
|
|
1414
1414
|
if (!shouldPromptForDeepSeek(args, process.cwd())) return true;
|
|
1415
|
-
console.log("No main model API key is configured for this project.");
|
|
1416
|
-
console.log("Choose DeepSeek, OpenAI, OpenRouter, Qwen, or Venice, then paste a key to save in
|
|
1415
|
+
console.log("No main model API key is configured for this account or project.");
|
|
1416
|
+
console.log("Choose DeepSeek, OpenAI, OpenRouter, Qwen, or Venice, then paste a key to save account-wide in `~/.agintiflow/.env` with 0600 permissions. Use `aginti auth --project` for a project override.");
|
|
1417
1417
|
const result = await runAuthWizard(process.cwd(), { provider: args.provider || "" });
|
|
1418
1418
|
printAuthWizardResult(result);
|
|
1419
1419
|
if (result.saved.some((item) => item.provider !== "grsai")) {
|
|
@@ -1424,7 +1424,9 @@ async function ensureDeepSeekKeyForOneShot(args) {
|
|
|
1424
1424
|
}
|
|
1425
1425
|
|
|
1426
1426
|
async function handleKeyCommand(argv) {
|
|
1427
|
-
const
|
|
1427
|
+
const scope = argv.includes("--project") || argv.includes("--local") ? "project" : "global";
|
|
1428
|
+
const cleanArgv = argv.filter((item) => !["--project", "--local", "--global"].includes(item));
|
|
1429
|
+
const [verb = "status", provider = ""] = cleanArgv;
|
|
1428
1430
|
if (verb === "status") {
|
|
1429
1431
|
const status = providerKeyStatus(process.cwd());
|
|
1430
1432
|
console.log(
|
|
@@ -1432,9 +1434,10 @@ async function handleKeyCommand(argv) {
|
|
|
1432
1434
|
status.openai ? "available" : "missing"
|
|
1433
1435
|
} qwen=${status.qwen ? "available" : "missing"} venice=${
|
|
1434
1436
|
status.venice ? "available" : "missing"
|
|
1435
|
-
} openrouter=${status.openrouter ? "available" : "missing"} grsai=${status.grsai ? "available" : "missing"} mock=available
|
|
1437
|
+
} openrouter=${status.openrouter ? "available" : "missing"} grsai=${status.grsai ? "available" : "missing"} mock=available globalEnv=${status.globalEnv ? "yes" : "no"} projectEnv=${status.projectEnv ? "yes" : "no"}`
|
|
1436
1438
|
);
|
|
1437
1439
|
console.log("env vars: DeepSeek=DEEPSEEK_API_KEY or LLM_API_KEY; OpenAI=OPENAI_API_KEY or LLM_API_KEY; OpenRouter=OPENROUTER_API_KEY; Qwen=QWEN_API_KEY; Venice=VENICE_API_KEY; image=GRSAI or GRSAI_API_KEY");
|
|
1440
|
+
console.log("storage: `aginti keys set` saves account-wide to ~/.agintiflow/.env; add --project for current .aginti/.env override.");
|
|
1438
1441
|
return;
|
|
1439
1442
|
}
|
|
1440
1443
|
|
|
@@ -1445,19 +1448,19 @@ async function handleKeyCommand(argv) {
|
|
|
1445
1448
|
console.error("No key saved.");
|
|
1446
1449
|
process.exit(1);
|
|
1447
1450
|
}
|
|
1448
|
-
const result = await setProviderKey(process.cwd(), target, key);
|
|
1449
|
-
console.log(`saved ${result.provider} key to project-local ignored env (${result.keyName})`);
|
|
1451
|
+
const result = await setProviderKey(process.cwd(), target, key, { scope });
|
|
1452
|
+
console.log(`saved ${result.provider} key to ${result.scope === "project" ? "project-local ignored env" : "account-wide ignored env"} (${result.keyName})`);
|
|
1450
1453
|
return;
|
|
1451
1454
|
}
|
|
1452
1455
|
|
|
1453
|
-
console.error("Usage: aginti keys status OR aginti keys set deepseek|openai|openrouter|qwen|venice|grsai [--stdin]");
|
|
1456
|
+
console.error("Usage: aginti keys status OR aginti keys set [--project|--global] deepseek|openai|openrouter|qwen|venice|grsai [--stdin]");
|
|
1454
1457
|
process.exit(1);
|
|
1455
1458
|
}
|
|
1456
1459
|
|
|
1457
1460
|
function printAuthWizardResult(result) {
|
|
1458
1461
|
if (result.saved.length > 0) {
|
|
1459
1462
|
for (const item of result.saved) {
|
|
1460
|
-
console.log(`saved ${item.provider} key to project-local ignored env (${item.keyName})`);
|
|
1463
|
+
console.log(`saved ${item.provider} key to ${item.scope === "project" ? "project-local ignored env" : "account-wide ignored env"} (${item.keyName})`);
|
|
1461
1464
|
}
|
|
1462
1465
|
}
|
|
1463
1466
|
if (result.saved.length === 0) console.log("No key saved.");
|
|
@@ -2184,9 +2187,11 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
2184
2187
|
}
|
|
2185
2188
|
|
|
2186
2189
|
if (commandArgv[0] === "auth" || commandArgv[0] === "login") {
|
|
2187
|
-
const
|
|
2190
|
+
const scope = commandArgv.includes("--project") || commandArgv.includes("--local") ? "project" : "global";
|
|
2191
|
+
const authArgv = commandArgv.slice(1).filter((item) => !["--project", "--local", "--global"].includes(item));
|
|
2192
|
+
const provider = normalizeAuthProvider(authArgv[0] || "", "");
|
|
2188
2193
|
if (commandArgv[0] === "auth" || (!provider && process.stdin.isTTY)) {
|
|
2189
|
-
const result = await runAuthWizard(commandCwd, { provider });
|
|
2194
|
+
const result = await runAuthWizard(commandCwd, { provider, scope });
|
|
2190
2195
|
printAuthWizardResult(result);
|
|
2191
2196
|
return;
|
|
2192
2197
|
}
|
|
@@ -2198,8 +2203,8 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
2198
2203
|
console.error("No key saved.");
|
|
2199
2204
|
process.exit(1);
|
|
2200
2205
|
}
|
|
2201
|
-
const result = await setProviderKey(commandCwd, target, key);
|
|
2202
|
-
console.log(`saved ${result.provider} key to project-local ignored env (${result.keyName})`);
|
|
2206
|
+
const result = await setProviderKey(commandCwd, target, key, { scope });
|
|
2207
|
+
console.log(`saved ${result.provider} key to ${result.scope === "project" ? "project-local ignored env" : "account-wide ignored env"} (${result.keyName})`);
|
|
2203
2208
|
return;
|
|
2204
2209
|
}
|
|
2205
2210
|
|
package/src/i18n.js
CHANGED
|
@@ -121,7 +121,7 @@ const TRANSLATIONS = {
|
|
|
121
121
|
helpTitle: "Commands:",
|
|
122
122
|
helpHelp: "Show this help.",
|
|
123
123
|
helpStatus: "Show active route, workspace, sandbox, and session.",
|
|
124
|
-
helpLogin: "Pick, paste, and save
|
|
124
|
+
helpLogin: "Pick, paste, and save account-wide API keys.",
|
|
125
125
|
helpInstructions: "Show AGINTI.md project instructions status.",
|
|
126
126
|
helpModels: "Show route/main/spare/wrapper/auxiliary model roles.",
|
|
127
127
|
helpVenice: "Pick Venice route/main models, or restore DeepSeek defaults.",
|
package/src/interactive-cli.js
CHANGED
|
@@ -1091,7 +1091,7 @@ function printHelp() {
|
|
|
1091
1091
|
tr("helpTitle"),
|
|
1092
1092
|
` ${command("/help", "Show this help.", "helpHelp")}`,
|
|
1093
1093
|
` ${command("/status", "Show active route, workspace, sandbox, and session.", "helpStatus")}`,
|
|
1094
|
-
` ${command("/login [deepseek|openai|openrouter|qwen|venice|grsai]", "Pick, paste, and save
|
|
1094
|
+
` ${command("/login [deepseek|openai|openrouter|qwen|venice|grsai]", "Pick, paste, and save account-wide API keys.", "helpLogin")}`,
|
|
1095
1095
|
` ${command("/auth [deepseek|openai|openrouter|qwen|venice|grsai]", "Alias for /login.", "helpLogin")}`,
|
|
1096
1096
|
` ${command("/instructions", "Show AGINTI.md project instructions status.", "helpInstructions")}`,
|
|
1097
1097
|
` ${command("/memory", "Alias for /instructions.", "helpInstructions")}`,
|
|
@@ -3506,8 +3506,9 @@ async function maybeOnboardDeepSeekKey(state) {
|
|
|
3506
3506
|
|
|
3507
3507
|
printAgentMessage(
|
|
3508
3508
|
[
|
|
3509
|
-
"No main model API key is configured for this project.",
|
|
3510
|
-
"Choose DeepSeek, OpenAI, OpenRouter, or
|
|
3509
|
+
"No main model API key is configured for this account or project.",
|
|
3510
|
+
"Choose DeepSeek, OpenAI, OpenRouter, Qwen, or Venice, then paste a key to save account-wide in `~/.agintiflow/.env` with 0600 permissions.",
|
|
3511
|
+
"Use `aginti auth --project` when a project needs an override in `.aginti/.env`.",
|
|
3511
3512
|
"After that, you can optionally paste the auxiliary image key. Press Esc to skip.",
|
|
3512
3513
|
].join("\n")
|
|
3513
3514
|
);
|
|
@@ -3535,7 +3536,9 @@ function applyAuthWizardResult(result, state = null) {
|
|
|
3535
3536
|
}
|
|
3536
3537
|
if (result.saved.length > 0) {
|
|
3537
3538
|
printAgentMessage(
|
|
3538
|
-
result.saved
|
|
3539
|
+
result.saved
|
|
3540
|
+
.map((item) => `Saved ${item.keyName} to ${item.scope === "project" ? "project-local ignored env" : "account-wide ignored env"}. Raw key was not printed.`)
|
|
3541
|
+
.join("\n")
|
|
3539
3542
|
);
|
|
3540
3543
|
} else {
|
|
3541
3544
|
printAgentMessage("No key saved.");
|
package/src/project.js
CHANGED
|
@@ -110,6 +110,7 @@ export function projectPaths(projectRoot = process.cwd()) {
|
|
|
110
110
|
sessionDbPath: path.join(root, PROJECT_SESSIONS_DIR_NAME, "web-state.sqlite"),
|
|
111
111
|
legacySessionDbPath: path.join(root, LEGACY_PROJECT_SESSIONS_DIR_NAME, "web-state.sqlite"),
|
|
112
112
|
agintiflowHome: globalPaths.home,
|
|
113
|
+
globalEnvPath: path.join(globalPaths.home, ".env"),
|
|
113
114
|
globalSessionsDir: globalPaths.sessionsDir,
|
|
114
115
|
globalSessionIndexPath: globalPaths.indexDbPath,
|
|
115
116
|
gitignorePath: path.join(root, ".gitignore"),
|
|
@@ -257,18 +258,38 @@ function ensureSecretGitignoreLinesSync(gitignorePath) {
|
|
|
257
258
|
fs.writeFileSync(gitignorePath, `${current}${prefix}${missing.join("\n")}\n`, "utf8");
|
|
258
259
|
}
|
|
259
260
|
|
|
260
|
-
function
|
|
261
|
-
|
|
261
|
+
function envTextFromParsed(parsed = {}) {
|
|
262
|
+
return Object.entries(parsed)
|
|
263
|
+
.filter(([key]) => LOCAL_ENV_KEYS.has(key))
|
|
264
|
+
.map(([key, value]) => `${key}=${JSON.stringify(value)}`)
|
|
265
|
+
.join("\n");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function readEnvFileSync(envPath) {
|
|
262
269
|
try {
|
|
263
|
-
|
|
270
|
+
return parseEnvText(fs.readFileSync(envPath, "utf8"));
|
|
264
271
|
} catch {
|
|
265
|
-
|
|
272
|
+
return {};
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function writeEnvFileSync(envPath, parsed = {}) {
|
|
277
|
+
fs.mkdirSync(path.dirname(envPath), { recursive: true });
|
|
278
|
+
fs.writeFileSync(envPath, `${envTextFromParsed(parsed)}\n`, { mode: 0o600 });
|
|
279
|
+
try {
|
|
280
|
+
fs.chmodSync(envPath, 0o600);
|
|
281
|
+
} catch {
|
|
282
|
+
// chmod can fail on non-POSIX filesystems; the file still remains local to the user account.
|
|
266
283
|
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function persistAmbientProviderKeysSync(paths, sourceEnv = process.env) {
|
|
287
|
+
const parsed = readEnvFileSync(paths.globalEnvPath);
|
|
267
288
|
|
|
268
289
|
let changed = false;
|
|
269
290
|
const persistedKeys = [];
|
|
270
291
|
for (const keyName of AMBIENT_PROVIDER_KEYS) {
|
|
271
|
-
const value = String(
|
|
292
|
+
const value = String(sourceEnv[keyName] || "").trim();
|
|
272
293
|
if (!value || parsed[keyName]) continue;
|
|
273
294
|
parsed[keyName] = value;
|
|
274
295
|
persistedKeys.push(keyName);
|
|
@@ -276,43 +297,46 @@ function persistAmbientProviderKeysSync(paths) {
|
|
|
276
297
|
}
|
|
277
298
|
if (!changed) return { persisted: false, keys: [] };
|
|
278
299
|
|
|
279
|
-
|
|
280
|
-
ensureSecretGitignoreLinesSync(paths.gitignorePath);
|
|
281
|
-
const output = Object.entries(parsed)
|
|
282
|
-
.filter(([key]) => LOCAL_ENV_KEYS.has(key))
|
|
283
|
-
.map(([key, value]) => `${key}=${JSON.stringify(value)}`)
|
|
284
|
-
.join("\n");
|
|
285
|
-
fs.writeFileSync(paths.envPath, `${output}\n`, { mode: 0o600 });
|
|
286
|
-
try {
|
|
287
|
-
fs.chmodSync(paths.envPath, 0o600);
|
|
288
|
-
} catch {
|
|
289
|
-
// chmod can fail on non-POSIX filesystems; the file still remains local and gitignored.
|
|
290
|
-
}
|
|
300
|
+
writeEnvFileSync(paths.globalEnvPath, parsed);
|
|
291
301
|
return { persisted: true, keys: persistedKeys };
|
|
292
302
|
}
|
|
293
303
|
|
|
294
304
|
export function loadProjectEnv(projectRoot = process.cwd(), { override = false } = {}) {
|
|
295
305
|
const paths = projectPaths(projectRoot);
|
|
296
|
-
const
|
|
306
|
+
const originalEnvKeys = new Set(
|
|
307
|
+
[...LOCAL_ENV_KEYS].filter((key) => String(process.env[key] || "").trim())
|
|
308
|
+
);
|
|
309
|
+
const ambientSourceEnv = {};
|
|
310
|
+
for (const key of AMBIENT_PROVIDER_KEYS) {
|
|
311
|
+
if (String(process.env[key] || "").trim()) ambientSourceEnv[key] = process.env[key];
|
|
312
|
+
}
|
|
313
|
+
const envPaths = [paths.globalEnvPath, paths.rootEnvPath, paths.envPath];
|
|
297
314
|
const loadedPaths = [];
|
|
298
315
|
for (const envPath of envPaths) {
|
|
299
316
|
try {
|
|
300
317
|
const parsed = parseEnvText(fs.readFileSync(envPath, "utf8"));
|
|
301
318
|
if (Object.keys(parsed).length === 0) continue;
|
|
319
|
+
const isProjectEnv = envPath === paths.rootEnvPath || envPath === paths.envPath;
|
|
302
320
|
for (const [key, value] of Object.entries(parsed)) {
|
|
303
|
-
if (override || !process.env[key]
|
|
321
|
+
if (override || !process.env[key] || (isProjectEnv && !originalEnvKeys.has(key))) {
|
|
322
|
+
process.env[key] = value;
|
|
323
|
+
}
|
|
304
324
|
}
|
|
305
325
|
loadedPaths.push(envPath);
|
|
306
326
|
} catch {
|
|
307
327
|
// Ignore missing or unreadable optional local env files.
|
|
308
328
|
}
|
|
309
329
|
}
|
|
310
|
-
const ambient = persistAmbientProviderKeysSync(paths);
|
|
311
|
-
if (ambient.persisted && !loadedPaths.includes(paths.
|
|
330
|
+
const ambient = persistAmbientProviderKeysSync(paths, ambientSourceEnv);
|
|
331
|
+
if (ambient.persisted && !loadedPaths.includes(paths.globalEnvPath)) loadedPaths.unshift(paths.globalEnvPath);
|
|
312
332
|
return {
|
|
313
333
|
loaded: loadedPaths.length > 0,
|
|
314
334
|
path: paths.envPath,
|
|
315
335
|
paths: loadedPaths,
|
|
336
|
+
globalEnv: loadedPaths.includes(paths.globalEnvPath),
|
|
337
|
+
globalEnvPath: paths.globalEnvPath,
|
|
338
|
+
projectEnv: loadedPaths.includes(paths.rootEnvPath) || loadedPaths.includes(paths.envPath),
|
|
339
|
+
projectEnvPaths: loadedPaths.filter((item) => item === paths.rootEnvPath || item === paths.envPath),
|
|
316
340
|
ambientPersisted: ambient.persisted,
|
|
317
341
|
ambientKeys: ambient.keys,
|
|
318
342
|
};
|
|
@@ -387,6 +411,7 @@ export async function initProject(projectRoot = process.cwd(), { template = "dis
|
|
|
387
411
|
paths.envExamplePath,
|
|
388
412
|
[
|
|
389
413
|
"# Copy values into .aginti/.env. Never commit real secrets.",
|
|
414
|
+
"# Account-wide defaults can be saved with `aginti auth`; this project file overrides them.",
|
|
390
415
|
"DEEPSEEK_API_KEY=",
|
|
391
416
|
"OPENAI_API_KEY=",
|
|
392
417
|
"OPENAI_BASE_URL=https://api.openai.com/v1",
|
|
@@ -467,7 +492,12 @@ export function providerKeyStatus(projectRoot = process.cwd()) {
|
|
|
467
492
|
grsai: Boolean(process.env.GRSAI || process.env.GRSAI_API_KEY),
|
|
468
493
|
mock: true,
|
|
469
494
|
localEnv: env.loaded,
|
|
495
|
+
globalEnv: env.globalEnv,
|
|
496
|
+
projectEnv: env.projectEnv,
|
|
470
497
|
localEnvPath: env.path,
|
|
498
|
+
globalEnvPath: env.globalEnvPath,
|
|
499
|
+
ambientPersisted: env.ambientPersisted,
|
|
500
|
+
ambientKeys: env.ambientKeys,
|
|
471
501
|
envVars: {
|
|
472
502
|
openai: ["OPENAI_API_KEY", "LLM_API_KEY"],
|
|
473
503
|
deepseek: ["DEEPSEEK_API_KEY", "LLM_API_KEY"],
|
|
@@ -522,7 +552,7 @@ export function providerKeyPreview(projectRoot = process.cwd(), provider = "") {
|
|
|
522
552
|
};
|
|
523
553
|
}
|
|
524
554
|
|
|
525
|
-
export async function setProviderKey(projectRoot, provider, value) {
|
|
555
|
+
export async function setProviderKey(projectRoot, provider, value, options = {}) {
|
|
526
556
|
const normalizedProvider = String(provider || "").toLowerCase();
|
|
527
557
|
const aliases = {
|
|
528
558
|
auxiliary: "grsai",
|
|
@@ -555,31 +585,34 @@ export async function setProviderKey(projectRoot, provider, value) {
|
|
|
555
585
|
if (!keyValue) throw new Error("Key value is required.");
|
|
556
586
|
|
|
557
587
|
const paths = projectPaths(projectRoot);
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
588
|
+
const requestedScope = String(options.scope || process.env.AGINTI_KEY_SCOPE || process.env.AGINTIFLOW_KEY_SCOPE || "global").toLowerCase();
|
|
589
|
+
const scope = ["project", "local"].includes(requestedScope) ? "project" : "global";
|
|
590
|
+
const targetPath = scope === "project" ? paths.envPath : paths.globalEnvPath;
|
|
591
|
+
if (scope === "project") {
|
|
592
|
+
await fsp.mkdir(paths.controlDir, { recursive: true });
|
|
593
|
+
await ensureLine(paths.gitignorePath, [
|
|
594
|
+
".aginti/.env",
|
|
595
|
+
".aginti/.env.*",
|
|
596
|
+
"!.aginti/.env.example",
|
|
597
|
+
]);
|
|
598
|
+
}
|
|
564
599
|
let parsed = {};
|
|
565
600
|
try {
|
|
566
|
-
parsed = parseEnvText(await fsp.readFile(
|
|
601
|
+
parsed = parseEnvText(await fsp.readFile(targetPath, "utf8"));
|
|
567
602
|
} catch {
|
|
568
603
|
parsed = {};
|
|
569
604
|
}
|
|
570
605
|
parsed[keyName] = keyValue;
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
.join("\n");
|
|
575
|
-
await fsp.writeFile(paths.envPath, `${output}\n`, { mode: 0o600 });
|
|
576
|
-
await fsp.chmod(paths.envPath, 0o600).catch(() => {});
|
|
606
|
+
await fsp.mkdir(path.dirname(targetPath), { recursive: true });
|
|
607
|
+
await fsp.writeFile(targetPath, `${envTextFromParsed(parsed)}\n`, { mode: 0o600 });
|
|
608
|
+
await fsp.chmod(targetPath, 0o600).catch(() => {});
|
|
577
609
|
loadProjectEnv(projectRoot, { override: true });
|
|
578
610
|
return {
|
|
579
611
|
ok: true,
|
|
580
612
|
provider: canonicalProvider,
|
|
581
613
|
keyName,
|
|
582
|
-
path:
|
|
614
|
+
path: targetPath,
|
|
615
|
+
scope,
|
|
583
616
|
};
|
|
584
617
|
}
|
|
585
618
|
|
|
@@ -911,13 +944,18 @@ export async function doctorReport(projectRoot, packageVersion, config) {
|
|
|
911
944
|
globalSessionsDir: paths.globalSessionsDir,
|
|
912
945
|
globalSessionIndexPath: paths.globalSessionIndexPath,
|
|
913
946
|
localEnvPresent: keyStatus.localEnv,
|
|
947
|
+
globalEnvPresent: keyStatus.globalEnv,
|
|
948
|
+
projectEnvPresent: keyStatus.projectEnv,
|
|
914
949
|
},
|
|
915
950
|
keys: {
|
|
916
951
|
openai: keyStatus.openai,
|
|
917
952
|
deepseek: keyStatus.deepseek,
|
|
953
|
+
openrouter: keyStatus.openrouter,
|
|
918
954
|
qwen: keyStatus.qwen,
|
|
919
955
|
venice: keyStatus.venice,
|
|
920
956
|
grsai: keyStatus.grsai,
|
|
957
|
+
globalEnv: keyStatus.globalEnv,
|
|
958
|
+
projectEnv: keyStatus.projectEnv,
|
|
921
959
|
mock: true,
|
|
922
960
|
},
|
|
923
961
|
sandbox: dockerStatus,
|
package/src/skill-library.js
CHANGED
|
@@ -100,15 +100,19 @@ function normalizeExternalTools(value) {
|
|
|
100
100
|
export function loadSkillFile(filePath) {
|
|
101
101
|
const text = fs.readFileSync(filePath, "utf8");
|
|
102
102
|
const { meta, body } = parseFrontmatter(text, filePath);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
103
|
+
const id = String(meta.id || meta.name || "").trim();
|
|
104
|
+
if (!id) {
|
|
105
|
+
throw new Error(`${filePath}: id or name must be a non-empty string`);
|
|
106
|
+
}
|
|
107
|
+
const label = String(meta.label || titleCaseSkillId(id)).trim();
|
|
108
|
+
const description = String(meta.description || "").trim();
|
|
109
|
+
if (!description) {
|
|
110
|
+
throw new Error(`${filePath}: description must be a non-empty string`);
|
|
107
111
|
}
|
|
108
112
|
return {
|
|
109
|
-
id
|
|
110
|
-
label
|
|
111
|
-
description
|
|
113
|
+
id,
|
|
114
|
+
label,
|
|
115
|
+
description,
|
|
112
116
|
triggers: normalizeList(meta.triggers),
|
|
113
117
|
tools: normalizeList(meta.tools),
|
|
114
118
|
body,
|
package/src/skillmesh.js
CHANGED
|
@@ -321,6 +321,60 @@ function canonicalSkillContent(content = "") {
|
|
|
321
321
|
return String(content || "").replace(/\r\n/g, "\n").trim();
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
+
function titleCaseSkillId(id = "") {
|
|
325
|
+
return String(id || "")
|
|
326
|
+
.split(/[-_\s/]+/)
|
|
327
|
+
.filter(Boolean)
|
|
328
|
+
.map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)
|
|
329
|
+
.join(" ");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function parseSkillMarkdownContent(content = "", context = "SKILL.md") {
|
|
333
|
+
const text = String(content || "");
|
|
334
|
+
if (!text.trim()) throw new Error(`${context}: empty skill`);
|
|
335
|
+
if (Buffer.byteLength(text, "utf8") > MAX_SKILL_BYTES) throw new Error(`${context}: skill is too large`);
|
|
336
|
+
if (redactSensitiveText(text) !== text) throw new Error(`${context}: contains token-like secret text`);
|
|
337
|
+
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
338
|
+
if (!match) throw new Error(`${context}: missing YAML frontmatter`);
|
|
339
|
+
const frontmatter = match[1].replace(/\r\n/g, "\n");
|
|
340
|
+
const fields = {};
|
|
341
|
+
const passthrough = [];
|
|
342
|
+
for (const line of frontmatter.split(/\n/)) {
|
|
343
|
+
const scalar = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
344
|
+
if (scalar) {
|
|
345
|
+
const key = scalar[1];
|
|
346
|
+
const value = scalar[2].trim().replace(/^['"]|['"]$/g, "");
|
|
347
|
+
if (value) fields[key] = value;
|
|
348
|
+
if (["id", "name", "label", "description"].includes(key)) continue;
|
|
349
|
+
}
|
|
350
|
+
passthrough.push(line);
|
|
351
|
+
}
|
|
352
|
+
const id = fields.id || fields.name || "";
|
|
353
|
+
if (!id) throw new Error(`${context}: id or name is required`);
|
|
354
|
+
if (!safeSkillId(id)) throw new Error(`${context}: invalid skill id ${id}`);
|
|
355
|
+
const description = fields.description || "";
|
|
356
|
+
if (!description) throw new Error(`${context}: description is required`);
|
|
357
|
+
const label = fields.label || titleCaseSkillId(id);
|
|
358
|
+
return {
|
|
359
|
+
fields: { ...fields, id, label, description },
|
|
360
|
+
body: text.slice(match[0].length).trim(),
|
|
361
|
+
passthrough: passthrough.join("\n").trim(),
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function normalizeSkillMarkdownForSkillMesh(content = "", context = "SKILL.md") {
|
|
366
|
+
const parsed = parseSkillMarkdownContent(content, context);
|
|
367
|
+
const frontmatter = [
|
|
368
|
+
"---",
|
|
369
|
+
`id: ${parsed.fields.id}`,
|
|
370
|
+
`label: ${parsed.fields.label}`,
|
|
371
|
+
`description: ${parsed.fields.description}`,
|
|
372
|
+
parsed.passthrough,
|
|
373
|
+
"---",
|
|
374
|
+
].filter((line) => line !== "");
|
|
375
|
+
return `${frontmatter.join("\n")}\n\n${parsed.body}`.trim();
|
|
376
|
+
}
|
|
377
|
+
|
|
324
378
|
function canonicalPackForSignature(pack) {
|
|
325
379
|
const copy = { ...pack };
|
|
326
380
|
delete copy.signature;
|
|
@@ -382,25 +436,7 @@ function verifyPackSignature(pack) {
|
|
|
382
436
|
}
|
|
383
437
|
|
|
384
438
|
function validateSkillMarkdownContent(content = "", context = "SKILL.md") {
|
|
385
|
-
|
|
386
|
-
if (!text.trim()) throw new Error(`${context}: empty skill`);
|
|
387
|
-
if (Buffer.byteLength(text, "utf8") > MAX_SKILL_BYTES) throw new Error(`${context}: skill is too large`);
|
|
388
|
-
if (redactSensitiveText(text) !== text) throw new Error(`${context}: contains token-like secret text`);
|
|
389
|
-
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
390
|
-
if (!match) throw new Error(`${context}: missing YAML frontmatter`);
|
|
391
|
-
const frontmatter = match[1];
|
|
392
|
-
const fields = {};
|
|
393
|
-
for (const line of frontmatter.split(/\r?\n/)) {
|
|
394
|
-
const scalar = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
395
|
-
if (!scalar) continue;
|
|
396
|
-
const value = scalar[2].trim().replace(/^['"]|['"]$/g, "");
|
|
397
|
-
if (value) fields[scalar[1]] = value;
|
|
398
|
-
}
|
|
399
|
-
for (const field of ["id", "label", "description"]) {
|
|
400
|
-
if (!fields[field]) throw new Error(`${context}: ${field} is required`);
|
|
401
|
-
}
|
|
402
|
-
if (!safeSkillId(fields.id)) throw new Error(`${context}: invalid skill id ${fields.id}`);
|
|
403
|
-
return fields;
|
|
439
|
+
return parseSkillMarkdownContent(content, context).fields;
|
|
404
440
|
}
|
|
405
441
|
|
|
406
442
|
function builtInSkillIds() {
|
|
@@ -420,7 +456,7 @@ export async function buildSkillPack(skillId, { home = agintiflowHome(), author
|
|
|
420
456
|
export async function buildSkillPackFromMarkdown(content, { home = agintiflowHome(), author = "local-user", valueScore = 80 } = {}) {
|
|
421
457
|
const fields = validateSkillMarkdownContent(content, "SKILL.md");
|
|
422
458
|
const id = fields.id;
|
|
423
|
-
const normalized =
|
|
459
|
+
const normalized = normalizeSkillMarkdownForSkillMesh(content, "SKILL.md");
|
|
424
460
|
const entry = {
|
|
425
461
|
id,
|
|
426
462
|
path: `skills/${id}/SKILL.md`,
|
package/web.js
CHANGED
|
@@ -652,6 +652,8 @@ function publicKeyStatus(projectRoot = baseDir) {
|
|
|
652
652
|
grsai: status.grsai,
|
|
653
653
|
mock: true,
|
|
654
654
|
localEnv: status.localEnv,
|
|
655
|
+
globalEnv: status.globalEnv,
|
|
656
|
+
projectEnv: status.projectEnv,
|
|
655
657
|
envVars: status.envVars,
|
|
656
658
|
};
|
|
657
659
|
}
|
|
@@ -1444,6 +1446,8 @@ app.get("/api/config", async (_req, res) => {
|
|
|
1444
1446
|
sharedSessionFolder: path.resolve(config.sessionsDir) === path.resolve(sessionsDir),
|
|
1445
1447
|
globalSessionIndexPath: paths.globalSessionIndexPath,
|
|
1446
1448
|
localEnvPresent: keyStatus.localEnv,
|
|
1449
|
+
globalEnvPresent: keyStatus.globalEnv,
|
|
1450
|
+
projectEnvPresent: keyStatus.projectEnv,
|
|
1447
1451
|
platform: {
|
|
1448
1452
|
...platform,
|
|
1449
1453
|
label: platformLabel(platform),
|
|
@@ -1648,6 +1652,7 @@ app.post("/api/keys/:provider", async (req, res) => {
|
|
|
1648
1652
|
ok: true,
|
|
1649
1653
|
provider: result.provider,
|
|
1650
1654
|
keyName: result.keyName,
|
|
1655
|
+
scope: result.scope,
|
|
1651
1656
|
keyStatus: publicKeyStatus(baseDir),
|
|
1652
1657
|
});
|
|
1653
1658
|
} catch (error) {
|