@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 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 the ignored project-local `.aginti/.env` file with restricted permissions. You can rerun setup any time:
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 keys with restricted permissions. Ignored by git. |
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
- Project-local keys:
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 project-locally:
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 `.aginti/.env` as `GRSAI` or `VENICE_API_KEY` with `0600` permissions. The CLI and web app only report whether a key exists; they never return raw values.
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
 
@@ -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 set per project and stored only in ignored `.aginti/.env`:
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.190",
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 a project-local model key.",
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. Mock mode remains available for local tests.",
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 local key",
34
- keySavedStatus: "Local key saved. Raw values are never returned by the API.",
35
- keySaveFailed: "Failed to save local key.",
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
@@ -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.localEnv, "ambient provider keys were not persisted into local .aginti/.env");
95
- const localEnv = await fs.readFile(path.join(tempRoot, ".aginti", ".env"), "utf8");
96
- assert(localEnv.includes("DEEPSEEK_API_KEY="), "ambient DeepSeek key was not saved locally");
97
- assert(localEnv.includes("OPENROUTER_API_KEY="), "ambient OpenRouter key was not saved locally");
98
- assert(localEnv.includes("VENICE_API_KEY="), "ambient Venice key was not saved locally");
99
- assert(localEnv.includes("GRSAI_API_KEY="), "ambient GRS AI key was not saved locally");
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
- "ambient-key-autopersist",
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,
@@ -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
- ? "Existing key is selected. Type to replace, Enter/Esc to keep existing."
467
- : "Paste key and Enter to save, or Esc/Enter to skip.",
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 ? "Existing key is selected. Type to replace, Enter/Esc to keep existing." : "Paste key and Enter to save, or Esc/Enter to skip.",
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 ? "Existing key is selected. Type to replace, Enter/Esc to keep existing." : "Paste key and Enter to save, or Esc/Enter to skip.",
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
 
@@ -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
- return normalizeImageProvider(
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
- throw new Error("Missing GRSAI key. Run `aginti login grsai`, `aginti keys set grsai --stdin`, or `/auxiliary grsai`.");
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`;
@@ -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 localEnv=${report.keys.localEnv}`
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 localEnv=${report.project.localEnvPresent}`
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 `.aginti/.env` with 0600 permissions.");
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 [verb = "status", provider = ""] = argv;
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 localEnv=${status.localEnv}`
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 provider = normalizeAuthProvider(commandArgv[1] || "", "");
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 project-local API keys.",
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.",
@@ -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 project-local API keys.", "helpLogin")}`,
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 Qwen, then paste a key to save in `.aginti/.env` with 0600 permissions.",
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.map((item) => `Saved ${item.keyName} to project-local ignored env. Raw key was not printed.`).join("\n")
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 persistAmbientProviderKeysSync(paths) {
261
- let parsed = {};
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
- parsed = parseEnvText(fs.readFileSync(paths.envPath, "utf8"));
270
+ return parseEnvText(fs.readFileSync(envPath, "utf8"));
264
271
  } catch {
265
- parsed = {};
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(process.env[keyName] || "").trim();
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
- fs.mkdirSync(paths.controlDir, { recursive: true });
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 envPaths = [paths.rootEnvPath, paths.envPath];
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]) process.env[key] = value;
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.envPath)) loadedPaths.push(paths.envPath);
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
- await fsp.mkdir(paths.controlDir, { recursive: true });
559
- await ensureLine(paths.gitignorePath, [
560
- ".aginti/.env",
561
- ".aginti/.env.*",
562
- "!.aginti/.env.example",
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(paths.envPath, "utf8"));
601
+ parsed = parseEnvText(await fsp.readFile(targetPath, "utf8"));
567
602
  } catch {
568
603
  parsed = {};
569
604
  }
570
605
  parsed[keyName] = keyValue;
571
- const output = Object.entries(parsed)
572
- .filter(([key]) => LOCAL_ENV_KEYS.has(key))
573
- .map(([key, envValue]) => `${key}=${JSON.stringify(envValue)}`)
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: paths.envPath,
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,
@@ -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
- for (const field of ["id", "label", "description"]) {
104
- if (typeof meta[field] !== "string" || !meta[field].trim()) {
105
- throw new Error(`${filePath}: ${field} must be a non-empty string`);
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: meta.id.trim(),
110
- label: meta.label.trim(),
111
- description: meta.description.trim(),
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
- const text = String(content || "");
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 = canonicalSkillContent(content);
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) {