aegiscode 6.3.1 → 6.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -8
- package/bin/aegiscode.js +161 -2
- package/package.json +2 -2
- package/src/app.js +193 -44
- package/src/chatflow.js +7 -4
- package/src/cloudsync.js +401 -0
- package/src/commands.js +304 -24
- package/src/config.js +6 -1
- package/src/credentials.js +323 -0
- package/src/history.js +34 -2
- package/src/overlays.js +18 -5
- package/src/panels.js +3 -1
- package/src/screens.js +159 -9
- package/src/secret.js +56 -0
- package/vendor/client/aegis.js +16 -1
- package/vendor/desktop/lib/local/engine.js +12 -7
package/README.md
CHANGED
|
@@ -38,12 +38,22 @@ npm install -g aegiscode
|
|
|
38
38
|
node cli/bin/aegiscode.js
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
Requires Node 18+.
|
|
41
|
+
Requires Node 18+. Save your key once — it goes to `~/.aegiscode/credentials.json`
|
|
42
|
+
(mode `0600`) and is then picked up by every later launch, script and shell:
|
|
42
43
|
|
|
43
44
|
```bash
|
|
44
|
-
|
|
45
|
+
aegiscode login # prompts, no echo
|
|
46
|
+
aegiscode login "aegis_..." # or inline
|
|
47
|
+
aegiscode key status # masked key + which source is in use
|
|
48
|
+
aegiscode logout # remove it
|
|
45
49
|
```
|
|
46
50
|
|
|
51
|
+
`AEGIS_API_KEY` still works and still wins (useful in CI, or for a one-off);
|
|
52
|
+
`aegiscode --key <key>` applies to a single run and is never saved. A key an
|
|
53
|
+
older AEGIS CLI left in `~/.aegiscode/config.json` is picked up and copied into
|
|
54
|
+
the 0600 store automatically — `/cloud status` says so if the plaintext copy is
|
|
55
|
+
still there.
|
|
56
|
+
|
|
47
57
|
## Use
|
|
48
58
|
|
|
49
59
|
```bash
|
|
@@ -101,8 +111,8 @@ command for command — 75 entries across nine categories:
|
|
|
101
111
|
| Session & context | `/clear` `/compact` `/cost` `/exit` `/new` `/recap` `/resume` `/rewind` `/agents` `/status` `/teleport` `/version` `/clone` `/schedule` |
|
|
102
112
|
| Workspace | `/run` `/build` `/cd` `/copy` `/init` `/review` `/prs` |
|
|
103
113
|
| Model & behavior | `/model` `/effort` `/thinking` `/theme` `/vim` `/router` `/confirm` `/yolo` `/permissions` `/hooks` `/skills` `/mcp` |
|
|
104
|
-
| Data | `/context` `/export` `/tokens` |
|
|
105
|
-
| Auth | `/credentials` `/byok` `/byok-set` `/byok-rm` |
|
|
114
|
+
| Data | `/context` `/export` `/tokens` `/sync` |
|
|
115
|
+
| Auth | `/key` `/login` `/logout` `/credentials` `/byok` `/byok-set` `/byok-rm` |
|
|
106
116
|
| Support | `/help` `/doctor` `/troubleshooting` `/feedback` `/bug` `/issue` `/onboarding` `/benchmark` `/release-notes` `/billing` `/cloud` |
|
|
107
117
|
| Aegis plugin | `/aegis-ask` `/aegis-status` `/aegis-recall` `/aegis-remember` `/memory` `/aegis-council` `/aegis-multi` `/aegis-print` `/aegis-import` `/tool` |
|
|
108
118
|
| Fun | `/radio` `/waifu` |
|
|
@@ -119,10 +129,35 @@ the project and writes `AEGIS.md`, `/export` writes the transcript out,
|
|
|
119
129
|
`/resume` and `/rewind` read the session store, `/cd` moves the working
|
|
120
130
|
directory, `/doctor` runs diagnostics.
|
|
121
131
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
132
|
+
## Account key and cloud sync
|
|
133
|
+
|
|
134
|
+
Three ways in, one store: `aegiscode login <key>`, `/key <api_key>` (or a bare
|
|
135
|
+
`/key` to paste it echo-off), and `$AEGIS_API_KEY`. The first two persist;
|
|
136
|
+
the env var outranks the store and needs no persistence. `/login` used to be an
|
|
137
|
+
unavailable Claude Code auth-loop command pointing at `/byok-set` — which stores
|
|
138
|
+
a *provider* key, so that advice sent the AEGIS key into the wrong slot. It is
|
|
139
|
+
now the in-band way to set the account key, and `/logout` removes it.
|
|
140
|
+
|
|
141
|
+
Conversation sync is the same `conversationSyncPush/Pull` surface the desktop
|
|
142
|
+
uses, over the sessions this host already keeps in `~/.aegiscode/history.jsonl`:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
/sync # push what is pending, then pull — the one you want
|
|
146
|
+
/sync status # local / pending / in-sync counts, last push and pull
|
|
147
|
+
/sync on | off # auto-push after each turn (off by default)
|
|
148
|
+
/cloud # key + sync state in one panel
|
|
149
|
+
/cloud activate # turn on cloud memory for the account
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Sync is **off by default** on purpose: the server charges a push for the growth
|
|
153
|
+
of a session against the plan's synced-token ceiling, so nothing is uploaded
|
|
154
|
+
until you ask. A refusal (HTTP 402) is reported as a quota refusal with the way
|
|
155
|
+
out, not as "sync failed". Imported remote sessions land in the same store
|
|
156
|
+
`/resume` reads, and are marked `imported` with estimated token counts — never
|
|
157
|
+
passed off as measured here.
|
|
158
|
+
|
|
159
|
+
There are currently **no** `unavailable` commands: every entry in the registry
|
|
160
|
+
either runs or is a real handler that says honestly what it cannot do.
|
|
126
161
|
|
|
127
162
|
## The chatflow
|
|
128
163
|
|
package/bin/aegiscode.js
CHANGED
|
@@ -24,10 +24,16 @@ Usage:
|
|
|
24
24
|
aegiscode -p "question" same, explicit
|
|
25
25
|
echo "q" | aegiscode -p - read the prompt from stdin
|
|
26
26
|
|
|
27
|
+
Account:
|
|
28
|
+
aegiscode login [<key>] save your AEGIS API key (prompts, no echo, if omitted)
|
|
29
|
+
aegiscode logout remove the saved key
|
|
30
|
+
aegiscode key status show which key is in use and where it came from
|
|
31
|
+
|
|
27
32
|
Options:
|
|
28
33
|
-m, --model <id> pin a model id (see /models; default: server choice)
|
|
29
34
|
--base <url> API base (default $AEGIS_API_BASE or aegiscloud.org)
|
|
30
|
-
--key <key> API key for
|
|
35
|
+
--key <key> API key for THIS RUN only — it is not saved. Use
|
|
36
|
+
"aegiscode login" to store one for good.
|
|
31
37
|
--json with -p: emit JSON instead of text
|
|
32
38
|
--no-stream buffer the answer instead of streaming it
|
|
33
39
|
--max-tokens <n> output ceiling hint
|
|
@@ -59,9 +65,22 @@ function parseArgs(argv) {
|
|
|
59
65
|
prompt: null,
|
|
60
66
|
help: false,
|
|
61
67
|
version: false,
|
|
68
|
+
command: null,
|
|
69
|
+
commandArg: null,
|
|
62
70
|
};
|
|
63
71
|
const rest = [];
|
|
64
72
|
|
|
73
|
+
// An account subcommand is `argv[0]` and nothing else — position 0 only, so
|
|
74
|
+
// `aegiscode -p "key"` (a one-shot prompt whose text happens to be one of
|
|
75
|
+
// these words) is never hijacked into an account operation.
|
|
76
|
+
const head = argv[0];
|
|
77
|
+
if (head && !head.startsWith('-') && ACCOUNT_COMMANDS.has(head)) {
|
|
78
|
+
const arg = argv[1];
|
|
79
|
+
opts.command = head;
|
|
80
|
+
opts.commandArg = arg && !arg.startsWith('-') ? arg : null;
|
|
81
|
+
argv = argv.slice(opts.commandArg ? 2 : 1);
|
|
82
|
+
}
|
|
83
|
+
|
|
65
84
|
for (let i = 0; i < argv.length; i++) {
|
|
66
85
|
const a = argv[i];
|
|
67
86
|
const next = () => {
|
|
@@ -127,6 +146,134 @@ function parseArgs(argv) {
|
|
|
127
146
|
return opts;
|
|
128
147
|
}
|
|
129
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Account subcommands: the in-band way to give this host a key.
|
|
151
|
+
*
|
|
152
|
+
* They exist because an `AEGIS_API_KEY` export was previously the *only* way
|
|
153
|
+
* in, and an export does not survive a new terminal. `--key` remains purely
|
|
154
|
+
* per-run (CI, a rotation test) so there is exactly one thing a user has to
|
|
155
|
+
* remember: `aegiscode login` saves it, `aegiscode logout` removes it.
|
|
156
|
+
*/
|
|
157
|
+
const ACCOUNT_COMMANDS = new Set(['login', 'logout', 'key']);
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Run `login` / `logout` / `key` and return a process exit code.
|
|
161
|
+
*
|
|
162
|
+
* Exit codes are meaningful because these are the commands a script calls:
|
|
163
|
+
* 0 = done, 1 = the server refused the credential, 2 = no credential and no
|
|
164
|
+
* way to ask for one (no TTY).
|
|
165
|
+
*/
|
|
166
|
+
async function runAccountCommand(command, arg, io = {}) {
|
|
167
|
+
const stdout = io.stdout || process.stdout;
|
|
168
|
+
const stderr = io.stderr || process.stderr;
|
|
169
|
+
const stdin = io.stdin || process.stdin;
|
|
170
|
+
const { credentials, readSecret, maskKey } = loadAccountDeps();
|
|
171
|
+
|
|
172
|
+
const where = () => credentials.credentialsPath();
|
|
173
|
+
|
|
174
|
+
if (command === 'logout' || (command === 'key' && ['clear', 'remove', 'rm'].includes(String(arg || '').toLowerCase()))) {
|
|
175
|
+
const res = credentials.clearApiKey();
|
|
176
|
+
stdout.write(
|
|
177
|
+
res.cleared
|
|
178
|
+
? `aegiscode: key removed from ${res.path}\n`
|
|
179
|
+
: 'aegiscode: no saved key to remove\n'
|
|
180
|
+
);
|
|
181
|
+
if (credentials.legacyKeyOnDisk()) {
|
|
182
|
+
stdout.write(
|
|
183
|
+
'aegiscode: note — a plaintext copy is also in config.json, written by an older ' +
|
|
184
|
+
'AEGIS CLI; delete its "aegiscloud" block as well to finish removing it\n'
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (command === 'key' && ['status', 'show', ''].includes(String(arg || '').toLowerCase())) {
|
|
191
|
+
const st = credentials.keyStatus();
|
|
192
|
+
if (io.json) {
|
|
193
|
+
stdout.write(
|
|
194
|
+
JSON.stringify(
|
|
195
|
+
{
|
|
196
|
+
configured: st.configured,
|
|
197
|
+
source: st.source,
|
|
198
|
+
source_label: credentials.sourceLabel(st.source),
|
|
199
|
+
path: st.path,
|
|
200
|
+
file_mode: st.fileMode,
|
|
201
|
+
masked: st.configured ? maskKey(st.key) : null,
|
|
202
|
+
memory_token: st.memoryToken,
|
|
203
|
+
verified_at: st.verifiedAt,
|
|
204
|
+
plaintext_copy_in_config: st.legacyPlaintext,
|
|
205
|
+
},
|
|
206
|
+
null,
|
|
207
|
+
2
|
|
208
|
+
) + '\n'
|
|
209
|
+
);
|
|
210
|
+
return st.configured ? 0 : 1;
|
|
211
|
+
}
|
|
212
|
+
stdout.write(
|
|
213
|
+
[
|
|
214
|
+
`key: ${st.configured ? maskKey(st.key) : 'not set'}`,
|
|
215
|
+
`source: ${credentials.sourceLabel(st.source)}`,
|
|
216
|
+
`file: ${st.path}${st.fileMode ? ` (${st.fileMode})` : ''}`,
|
|
217
|
+
`memory: ${st.memoryToken ? 'token held (cloud sync ready)' : 'no token'}`,
|
|
218
|
+
st.verifiedAt ? `verified: ${st.verifiedAt}` : null,
|
|
219
|
+
st.legacyPlaintext
|
|
220
|
+
? `note: a plaintext copy also sits in config.json — re-save with \`aegiscode login\` to move it`
|
|
221
|
+
: null,
|
|
222
|
+
]
|
|
223
|
+
.filter(Boolean)
|
|
224
|
+
.join('\n') + '\n'
|
|
225
|
+
);
|
|
226
|
+
return st.configured ? 0 : 1;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// `login [<key>]` and `key <api_key>`: take the key inline or ask for it.
|
|
230
|
+
let key = arg && arg !== 'status' ? String(arg).trim() : '';
|
|
231
|
+
if (!key) {
|
|
232
|
+
if (!stdin.isTTY) {
|
|
233
|
+
stderr.write(
|
|
234
|
+
'aegiscode: no terminal to prompt on — pass the key: `aegiscode login <api_key>`\n' +
|
|
235
|
+
' (or set AEGIS_API_KEY for a single run)\n'
|
|
236
|
+
);
|
|
237
|
+
return 2;
|
|
238
|
+
}
|
|
239
|
+
key = await readSecret('AEGIS API key (kept off screen, Enter to cancel): ', { stdin, stdout });
|
|
240
|
+
}
|
|
241
|
+
if (!key) {
|
|
242
|
+
stderr.write('aegiscode: no key given — nothing saved\n');
|
|
243
|
+
return 2;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const { createApp } = require('../src/app.js');
|
|
247
|
+
const app = createApp({ interactive: false });
|
|
248
|
+
const res = await app.setApiKey(key);
|
|
249
|
+
if (!res.ok) {
|
|
250
|
+
stderr.write(`aegiscode: ${res.message || 'that key could not be saved'}\n`);
|
|
251
|
+
return 2;
|
|
252
|
+
}
|
|
253
|
+
const who = res.account && (res.account.email || res.account.plan);
|
|
254
|
+
stdout.write(`aegiscode: key saved to ${res.path}${who ? ` — ${who}` : ''}\n`);
|
|
255
|
+
if (res.error) {
|
|
256
|
+
const status = res.error.status;
|
|
257
|
+
stderr.write(`aegiscode: the account check failed: ${res.error.message}\n`);
|
|
258
|
+
if (status === 401 || status === 403) {
|
|
259
|
+
stderr.write('aegiscode: the key is stored but the server refused it — re-run with a fresh key\n');
|
|
260
|
+
return 1;
|
|
261
|
+
}
|
|
262
|
+
stderr.write('aegiscode: (stored anyway — the server could not be reached to confirm it)\n');
|
|
263
|
+
return 0;
|
|
264
|
+
}
|
|
265
|
+
stdout.write('aegiscode: verified — run `aegiscode` to start a session\n');
|
|
266
|
+
return 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Lazily required so `--version`/`--help` stay dependency-free and instant. */
|
|
270
|
+
function loadAccountDeps() {
|
|
271
|
+
const credentials = require('../src/credentials.js');
|
|
272
|
+
const { readSecret } = require('../src/secret.js');
|
|
273
|
+
const { maskKey } = require('../src/format.js');
|
|
274
|
+
return { credentials, readSecret, maskKey };
|
|
275
|
+
}
|
|
276
|
+
|
|
130
277
|
function readStdin() {
|
|
131
278
|
return new Promise((resolve) => {
|
|
132
279
|
let buf = '';
|
|
@@ -163,6 +310,18 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
163
310
|
if (opts.base) process.env.AEGIS_API_BASE = opts.base;
|
|
164
311
|
if (opts.key) process.env.AEGIS_API_KEY = opts.key;
|
|
165
312
|
|
|
313
|
+
// Account subcommands run before a session is built: they are one-shot,
|
|
314
|
+
// non-interactive, and must work in a script (`aegiscode login "$KEY"`) as
|
|
315
|
+
// well as at a prompt.
|
|
316
|
+
if (ACCOUNT_COMMANDS.has(opts.command)) {
|
|
317
|
+
return runAccountCommand(opts.command, opts.commandArg, {
|
|
318
|
+
json: opts.json,
|
|
319
|
+
stdin: process.stdin,
|
|
320
|
+
stdout: process.stdout,
|
|
321
|
+
stderr: process.stderr,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
166
325
|
const { createApp } = require('../src/app.js');
|
|
167
326
|
|
|
168
327
|
let prompt = opts.prompt;
|
|
@@ -214,4 +373,4 @@ if (require.main === module) {
|
|
|
214
373
|
});
|
|
215
374
|
}
|
|
216
375
|
|
|
217
|
-
module.exports = { main, parseArgs, HELP };
|
|
376
|
+
module.exports = { main, parseArgs, HELP, ACCOUNT_COMMANDS, runAccountCommand };
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aegiscode",
|
|
3
3
|
"productName": "AEGIS Code",
|
|
4
|
-
"version": "6.
|
|
5
|
-
"description": "aegiscode
|
|
4
|
+
"version": "6.4.0",
|
|
5
|
+
"description": "aegiscode \u2014 the command-line version of AEGIS Desktop. The shared tool surface in your shell, over the same thin transport and tool registry as the MCP plugin and the desktop app. Ships transport + UI only; no brain.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "AEGIS Code",
|
|
8
8
|
"email": "nborneklint@gmail.com"
|
package/src/app.js
CHANGED
|
@@ -27,6 +27,9 @@ const { GLYPH, VERBS, themeOf, RESET, THEME_TABLE } = require('./theme.js');
|
|
|
27
27
|
const { LiveRegion, termWidth, w } = require('./screen.js');
|
|
28
28
|
const { parseLine, COMMANDS, visibleCommands } = require('./commands.js');
|
|
29
29
|
const { updateConfig, loadPermissions, loadConfig, configExists } = require('./config.js');
|
|
30
|
+
const credentials = require('./credentials.js');
|
|
31
|
+
const cloudsync = require('./cloudsync.js');
|
|
32
|
+
const { readSecret } = require('./secret.js');
|
|
30
33
|
const { normalizeModelCatalog, pickerEntries, catalogIds } = require('./models.js');
|
|
31
34
|
const { appendHistory, readSessionTranscript, readOwnSessions } = require('./history.js');
|
|
32
35
|
const { snapshotCheckpoint } = require('./checkpoint.js');
|
|
@@ -50,7 +53,12 @@ function createApp(options = {}) {
|
|
|
50
53
|
};
|
|
51
54
|
const out = options.out || process.stdout;
|
|
52
55
|
const err = options.err || process.stderr;
|
|
53
|
-
|
|
56
|
+
// The key comes from the credential store (env → credentials.json →
|
|
57
|
+
// config.json) rather than from the environment alone: an export does not
|
|
58
|
+
// survive a new shell, and until it is *stored* every one of those launches
|
|
59
|
+
// is "no key" with no in-band way to set one. An injected client keeps
|
|
60
|
+
// whatever key it was built with.
|
|
61
|
+
const client = options.client || createClient(credentials.clientOptions());
|
|
54
62
|
const { TOOLS, toolList } = options.tools || createTools(client);
|
|
55
63
|
|
|
56
64
|
// Tool-approval gate (exec/writeFile/editFile confirm before running).
|
|
@@ -87,7 +95,11 @@ function createApp(options = {}) {
|
|
|
87
95
|
const commandCtx = {
|
|
88
96
|
light: opts.light,
|
|
89
97
|
model: opts.model,
|
|
90
|
-
|
|
98
|
+
// `null` = "auto": send no effort, so the server sizes the turn from the
|
|
99
|
+
// ask. This is the *budget* control on the pooled class (aegis1 sizes the
|
|
100
|
+
// token ladder from it), which is why it is not defaulted to a rung here —
|
|
101
|
+
// pinning one would make every turn cost what that rung grants.
|
|
102
|
+
effort: null,
|
|
91
103
|
thinking: false,
|
|
92
104
|
themeIndex: opts.light ? 2 : 1,
|
|
93
105
|
vim: false,
|
|
@@ -147,6 +159,83 @@ function createApp(options = {}) {
|
|
|
147
159
|
}
|
|
148
160
|
}
|
|
149
161
|
|
|
162
|
+
// ── the AEGIS account key ─────────────────────────────────────────────────
|
|
163
|
+
//
|
|
164
|
+
// Set/forget at runtime, for `/key` and `aegiscode login`. Both paths are the
|
|
165
|
+
// same three steps — persist, apply to the live client, invalidate the model
|
|
166
|
+
// catalog — because a key that is stored but not applied leaves the current
|
|
167
|
+
// session failing, and one that is applied but not stored is gone next launch.
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Persist an account key and start using it immediately.
|
|
171
|
+
*
|
|
172
|
+
* `verify` spends one `/api/verify-api-key` round trip to catch a bad key at
|
|
173
|
+
* the moment it is entered rather than at the first prompt, and is also how
|
|
174
|
+
* the memory token (cloud sync's credential) is obtained — so a successful
|
|
175
|
+
* login leaves both credentials in the store.
|
|
176
|
+
*/
|
|
177
|
+
async function setApiKey(raw, { verify = true } = {}) {
|
|
178
|
+
const saved = credentials.saveApiKey(raw);
|
|
179
|
+
if (!saved.ok) return saved;
|
|
180
|
+
client.setApiKey(saved.key);
|
|
181
|
+
modelCache.models = [];
|
|
182
|
+
modelCache.at = 0;
|
|
183
|
+
let account = null;
|
|
184
|
+
let error = null;
|
|
185
|
+
if (verify) {
|
|
186
|
+
try {
|
|
187
|
+
const info = (await client.verifyApiKey()) || {};
|
|
188
|
+
account = info;
|
|
189
|
+
const patch = { verifiedAt: new Date().toISOString() };
|
|
190
|
+
if (info.memory_token) patch.memoryToken = info.memory_token;
|
|
191
|
+
if (info.plan) patch.plan = info.plan;
|
|
192
|
+
patch.account = { plan: info.plan || null, email: info.email || null, valid: info.valid !== false };
|
|
193
|
+
credentials.writeCredentials(patch);
|
|
194
|
+
if (info.plan) session.plan = info.plan;
|
|
195
|
+
if (info.email) session.account = { ...(session.account || {}), email: info.email };
|
|
196
|
+
} catch (e) {
|
|
197
|
+
error = e;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return { ...saved, account, error };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Drop the stored key and stop authenticating this session. */
|
|
204
|
+
function forgetApiKey() {
|
|
205
|
+
const res = credentials.clearApiKey();
|
|
206
|
+
client.setApiKey('');
|
|
207
|
+
modelCache.models = [];
|
|
208
|
+
modelCache.at = 0;
|
|
209
|
+
return res;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function cloudSyncEnabled() {
|
|
213
|
+
try {
|
|
214
|
+
return loadConfig().cloudSync === true;
|
|
215
|
+
} catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function setCloudSync(on) {
|
|
221
|
+
return updateConfig({ cloudSync: on === true });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Copy a key found in config.json into the 0600 store, once, so the next run
|
|
226
|
+
* reads the tight copy. Never deletes the original — that file belongs to
|
|
227
|
+
* another product and holds the user's key.
|
|
228
|
+
*/
|
|
229
|
+
function adoptLegacyKeyOnce() {
|
|
230
|
+
try {
|
|
231
|
+
const resolved = credentials.resolveApiKey();
|
|
232
|
+
if (resolved.source !== 'config') return [];
|
|
233
|
+
return credentials.adoptLegacy().adopted;
|
|
234
|
+
} catch {
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
150
239
|
// ── the AEGIS Cloud model catalog ──────────────────────────────────────────
|
|
151
240
|
//
|
|
152
241
|
// `/model` (and the alt+p chord that dispatches it) reads the pinnable ids
|
|
@@ -371,6 +460,14 @@ function createApp(options = {}) {
|
|
|
371
460
|
model: commandCtx.model || undefined,
|
|
372
461
|
system: opts.system,
|
|
373
462
|
maxTokens: opts.maxTokens,
|
|
463
|
+
// `effort` is the pooled budget control, and it travels on every turn
|
|
464
|
+
// — this host only ever runs the 'aegis' class, whose calls are sized
|
|
465
|
+
// server-side from it (aegis1 services/pool_brain.py). It used to be
|
|
466
|
+
// held in the session state and rendered in the status line without
|
|
467
|
+
// ever reaching the wire, so /effort changed the display and nothing
|
|
468
|
+
// about what the turn cost. `null` (auto) is omitted so the server
|
|
469
|
+
// infers the rung from the ask.
|
|
470
|
+
effort: commandCtx.effort || undefined,
|
|
374
471
|
// false asks the engine for the buffered (non-stream) wire form, so
|
|
375
472
|
// `--no-stream` and piped runs get a single body rather than SSE.
|
|
376
473
|
stream: commandCtx.stream !== false,
|
|
@@ -436,7 +533,7 @@ function createApp(options = {}) {
|
|
|
436
533
|
render.renderNotice(
|
|
437
534
|
ctx(),
|
|
438
535
|
'error',
|
|
439
|
-
|
|
536
|
+
`no AEGIS account key — ${credentials.HOW_TO_SET} (or set $${credentials.KEY_ENV})`
|
|
440
537
|
)
|
|
441
538
|
);
|
|
442
539
|
return;
|
|
@@ -494,7 +591,7 @@ function createApp(options = {}) {
|
|
|
494
591
|
const tool = TOOLS[name];
|
|
495
592
|
if (!tool) throw new Error(`unknown tool: ${name}`);
|
|
496
593
|
if (!client.apiKey) {
|
|
497
|
-
throw new Error(
|
|
594
|
+
throw new Error(`no AEGIS account key — ${credentials.HOW_TO_SET}`);
|
|
498
595
|
}
|
|
499
596
|
const text = await tool.run(args || {});
|
|
500
597
|
emit(render.renderToolResult(ctx(), name, text, width()));
|
|
@@ -713,6 +810,17 @@ function createApp(options = {}) {
|
|
|
713
810
|
TOOLS,
|
|
714
811
|
saveConfig: (patch) => updateConfig(patch),
|
|
715
812
|
showThemePicker: () => showThemePicker(),
|
|
813
|
+
// Credential + cloud-sync surface for the /key, /cloud and /sync commands.
|
|
814
|
+
// Defined here on the app's context so the chatflow's Object.assign-based
|
|
815
|
+
// context inherits them too — one definition, both hosts.
|
|
816
|
+
keyStatus: () => credentials.keyStatus(),
|
|
817
|
+
setApiKey: (raw, o) => setApiKey(raw, o),
|
|
818
|
+
forgetApiKey: () => forgetApiKey(),
|
|
819
|
+
readSecret: (text) => readSecret(text, { stdin: process.stdin, stdout: out }),
|
|
820
|
+
cloudsync,
|
|
821
|
+
cloudSyncEnabled: () => cloudSyncEnabled(),
|
|
822
|
+
setCloudSync: (on) => setCloudSync(on),
|
|
823
|
+
adoptLegacyKey: () => adoptLegacyKeyOnce(),
|
|
716
824
|
};
|
|
717
825
|
Object.defineProperty(c, 'sessionId', {
|
|
718
826
|
enumerable: true,
|
|
@@ -801,7 +909,7 @@ function createApp(options = {}) {
|
|
|
801
909
|
try {
|
|
802
910
|
let args = cmd.build(arg);
|
|
803
911
|
if (cmd.secret) {
|
|
804
|
-
const key = await
|
|
912
|
+
const key = await askSecret(`provider key for ${args.provider}: `);
|
|
805
913
|
if (!key) {
|
|
806
914
|
emit(render.renderNotice(ctx(), 'warn', 'empty key — nothing sent'));
|
|
807
915
|
return true;
|
|
@@ -816,52 +924,26 @@ function createApp(options = {}) {
|
|
|
816
924
|
return true;
|
|
817
925
|
}
|
|
818
926
|
|
|
819
|
-
/**
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
stdin.resume();
|
|
831
|
-
const onData = (chunk) => {
|
|
832
|
-
for (const ch of chunk.toString('utf8')) {
|
|
833
|
-
if (ch === '\r' || ch === '\n') {
|
|
834
|
-
stdin.setRawMode(false);
|
|
835
|
-
stdin.removeListener('data', onData);
|
|
836
|
-
out.write('\n');
|
|
837
|
-
return resolve(buf);
|
|
838
|
-
}
|
|
839
|
-
if (ch === '\x03') {
|
|
840
|
-
// ctrl+c
|
|
841
|
-
stdin.setRawMode(false);
|
|
842
|
-
stdin.removeListener('data', onData);
|
|
843
|
-
out.write('\n');
|
|
844
|
-
return resolve('');
|
|
845
|
-
}
|
|
846
|
-
if (ch === '\u007f') {
|
|
847
|
-
buf = buf.slice(0, -1);
|
|
848
|
-
out.write('\b \b');
|
|
849
|
-
continue;
|
|
850
|
-
}
|
|
851
|
-
buf += ch;
|
|
852
|
-
out.write('•');
|
|
853
|
-
}
|
|
854
|
-
};
|
|
855
|
-
stdin.on('data', onData);
|
|
856
|
-
});
|
|
927
|
+
/**
|
|
928
|
+
* Ask for a secret on this session's streams.
|
|
929
|
+
*
|
|
930
|
+
* The prompt loop itself lives in secret.js, shared with `aegiscode login` in
|
|
931
|
+
* the bin — one implementation, so the terminal host and the non-interactive
|
|
932
|
+
* entry point cannot differ in whether a key is echoed or masked. Only the
|
|
933
|
+
* prompt's colouring is the session's business.
|
|
934
|
+
*/
|
|
935
|
+
function askSecret(promptText) {
|
|
936
|
+
const t = themeOf(ctx());
|
|
937
|
+
return readSecret(t.gray + promptText + RESET, { stdin: process.stdin, stdout: out });
|
|
857
938
|
}
|
|
858
939
|
|
|
859
940
|
// --- entry points ---------------------------------------------------------
|
|
860
941
|
|
|
861
942
|
/** Non-interactive: one prompt, plain output, exit code. */
|
|
862
943
|
async function runOnce(prompt, { json = false } = {}) {
|
|
944
|
+
adoptLegacyKeyOnce();
|
|
863
945
|
if (!client.apiKey) {
|
|
864
|
-
err.write(
|
|
946
|
+
err.write(`aegiscode: no AEGIS account key. ${credentials.HOW_TO_SET}, or set $${credentials.KEY_ENV}.\n`);
|
|
865
947
|
return 2;
|
|
866
948
|
}
|
|
867
949
|
const res = await ask(prompt);
|
|
@@ -946,6 +1028,39 @@ function createApp(options = {}) {
|
|
|
946
1028
|
} catch {
|
|
947
1029
|
/* persistence is best-effort */
|
|
948
1030
|
}
|
|
1031
|
+
// Auto-sync hook: one place, reached by both the linear REPL and the
|
|
1032
|
+
// chatflow's session loop, and only when the user turned it on. Off by
|
|
1033
|
+
// default because a push is billed against the plan's synced-token ceiling
|
|
1034
|
+
// (aegis1 charges the *growth* of a session), so uploading is a choice.
|
|
1035
|
+
if (cloudSyncEnabled()) autoSync();
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* Push after a turn, in the background.
|
|
1040
|
+
*
|
|
1041
|
+
* Deliberately not awaited and deliberately quiet on success: this runs after
|
|
1042
|
+
* every turn, and a line per turn would be noise. Failures are reported once
|
|
1043
|
+
* per session per session-cycle — a quota refusal that repeats forever is a
|
|
1044
|
+
* wall of text, not information — and never thrown, because the user's turn
|
|
1045
|
+
* already succeeded by the time this runs.
|
|
1046
|
+
*/
|
|
1047
|
+
let autoSyncNotes = new Set();
|
|
1048
|
+
function autoSync() {
|
|
1049
|
+
if (!client.apiKey) return;
|
|
1050
|
+
Promise.resolve()
|
|
1051
|
+
.then(() => cloudsync.push(client))
|
|
1052
|
+
.then((res) => {
|
|
1053
|
+
if (res && res.failed && res.failed.length) {
|
|
1054
|
+
const f = res.failed[0];
|
|
1055
|
+
if (autoSyncNotes.has(f.kind)) return;
|
|
1056
|
+
autoSyncNotes.add(f.kind);
|
|
1057
|
+
const hint = f.hint ? ` — ${f.hint}` : '';
|
|
1058
|
+
emit(render.renderNotice(ctx(), f.kind === 'quota' ? 'warn' : 'error', `cloud sync: ${f.message}${hint}`));
|
|
1059
|
+
}
|
|
1060
|
+
})
|
|
1061
|
+
.catch(() => {
|
|
1062
|
+
/* offline is not worth interrupting a session over */
|
|
1063
|
+
});
|
|
949
1064
|
}
|
|
950
1065
|
|
|
951
1066
|
/** One line of session accounting, for ctrl+t and the meta row. */ function tokenSummary() {
|
|
@@ -1073,6 +1188,8 @@ function createApp(options = {}) {
|
|
|
1073
1188
|
return;
|
|
1074
1189
|
}
|
|
1075
1190
|
if (!opts.model && cfg.model) commandCtx.model = cfg.model;
|
|
1191
|
+
// `null` in the config is "auto" and is deliberately not assigned over the
|
|
1192
|
+
// default — there is nothing to restore, because nothing is pinned.
|
|
1076
1193
|
if (cfg.effort) commandCtx.effort = cfg.effort;
|
|
1077
1194
|
if (typeof cfg.vim === 'boolean') commandCtx.vim = cfg.vim;
|
|
1078
1195
|
if (cfg.lastRecap) commandCtx.lastRecap = cfg.lastRecap;
|
|
@@ -1101,6 +1218,7 @@ function createApp(options = {}) {
|
|
|
1101
1218
|
process.stdout.isTTY;
|
|
1102
1219
|
if (tty) {
|
|
1103
1220
|
restorePrefs();
|
|
1221
|
+
adoptLegacyKeyOnce();
|
|
1104
1222
|
// Onboarding runs in the normal buffer, before the session takes the
|
|
1105
1223
|
// alternate screen — the reference's order. A declined trust check must
|
|
1106
1224
|
// abort: the reference returns without ever reaching `session(ctx)`.
|
|
@@ -1108,8 +1226,31 @@ function createApp(options = {}) {
|
|
|
1108
1226
|
continue: !!options.continue,
|
|
1109
1227
|
seen: options.seen || configExists,
|
|
1110
1228
|
save: (patch) => updateConfig(patch),
|
|
1229
|
+
// With no key the session can do nothing, so ask for it here where the
|
|
1230
|
+
// user is already answering questions — verified on submit, and saved
|
|
1231
|
+
// 0600 so the next launch (and every script) has it.
|
|
1232
|
+
needsKey: () => !client.apiKey,
|
|
1233
|
+
submitKey: (key) => setApiKey(key),
|
|
1111
1234
|
});
|
|
1112
1235
|
if (!onboard.ok) return 0;
|
|
1236
|
+
if (onboard.key && onboard.key.set) {
|
|
1237
|
+
emit(
|
|
1238
|
+
render.renderNotice(
|
|
1239
|
+
ctx(),
|
|
1240
|
+
'info',
|
|
1241
|
+
`key saved to ${credentials.credentialsPath()} — no export needed from now on`
|
|
1242
|
+
)
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
if (onboard.key && onboard.key.skipped && !client.apiKey) {
|
|
1246
|
+
emit(
|
|
1247
|
+
render.renderNotice(
|
|
1248
|
+
ctx(),
|
|
1249
|
+
'warn',
|
|
1250
|
+
`no key saved — /key <api_key> or \`aegiscode login\` whenever you are ready`
|
|
1251
|
+
)
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1113
1254
|
// A stored pin the platform does not advertise routes elsewhere in
|
|
1114
1255
|
// silence (see validatePinnedModel) — checked once, here, where the user
|
|
1115
1256
|
// can act on it. Not on `-p`: a network round-trip ahead of the first
|
|
@@ -1129,6 +1270,7 @@ function createApp(options = {}) {
|
|
|
1129
1270
|
return 0;
|
|
1130
1271
|
}
|
|
1131
1272
|
restorePrefs();
|
|
1273
|
+
adoptLegacyKeyOnce();
|
|
1132
1274
|
return runLinearRepl();
|
|
1133
1275
|
}
|
|
1134
1276
|
|
|
@@ -1154,6 +1296,13 @@ function createApp(options = {}) {
|
|
|
1154
1296
|
recordTurn,
|
|
1155
1297
|
persistTurn,
|
|
1156
1298
|
restorePrefs,
|
|
1299
|
+
setApiKey,
|
|
1300
|
+
forgetApiKey,
|
|
1301
|
+
adoptLegacyKeyOnce,
|
|
1302
|
+
cloudSyncEnabled,
|
|
1303
|
+
setCloudSync,
|
|
1304
|
+
cloudsync,
|
|
1305
|
+
keyStatus: () => credentials.keyStatus(),
|
|
1157
1306
|
sessionId: () => commandCtx.sessionId,
|
|
1158
1307
|
tokenSummary,
|
|
1159
1308
|
resumeSession,
|
package/src/chatflow.js
CHANGED
|
@@ -164,7 +164,7 @@ const separatorLine = (cols, ctx) => [span(themeOf(ctx).dim, '─'.repeat(cols))
|
|
|
164
164
|
/** The idle bottom-left line: which effort level the next turn runs at. */
|
|
165
165
|
function effortLine(ctx, cols) {
|
|
166
166
|
const t = themeOf(ctx);
|
|
167
|
-
const txt = `● ${ctx.effort || '
|
|
167
|
+
const txt = `● ${ctx.effort || 'auto'} · /effort`;
|
|
168
168
|
const pad = ' '.repeat(Math.max(0, cols - [...txt].length - 4));
|
|
169
169
|
return [span(t.gray, pad + txt)];
|
|
170
170
|
}
|
|
@@ -1111,12 +1111,15 @@ async function runSession(host) {
|
|
|
1111
1111
|
return;
|
|
1112
1112
|
}
|
|
1113
1113
|
if (type === 'effort') {
|
|
1114
|
-
|
|
1115
|
-
|
|
1114
|
+
// The picker's own order, not a second copy of the level names: this
|
|
1115
|
+
// used to re-declare ['low','medium','high'] here while the picker drew
|
|
1116
|
+
// its own table, so the row a user picked and the value it stored were
|
|
1117
|
+
// resolved through different lists.
|
|
1118
|
+
const chosen = overlays.EFFORT_VALUES[overlay.sel || 0];
|
|
1116
1119
|
overlay = null;
|
|
1117
1120
|
ctx.effort = chosen;
|
|
1118
1121
|
host.updateConfig({ effort: chosen });
|
|
1119
|
-
note(`effort: ${chosen}`);
|
|
1122
|
+
note(`effort: ${chosen || 'auto'}`);
|
|
1120
1123
|
render();
|
|
1121
1124
|
return;
|
|
1122
1125
|
}
|