@agentic-kit/cli 0.8.2 → 0.9.1
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 +15 -2
- package/account-store.d.ts +19 -0
- package/account-store.js +59 -0
- package/api-key.d.ts +51 -0
- package/api-key.js +96 -0
- package/auth-error.d.ts +4 -0
- package/auth-error.js +114 -0
- package/auth.d.ts +28 -0
- package/auth.js +119 -0
- package/backend-store.d.ts +19 -0
- package/backend-store.js +64 -0
- package/commands.d.ts +3 -0
- package/commands.js +112 -0
- package/config.d.ts +16 -0
- package/config.js +53 -1
- package/credentials.d.ts +11 -0
- package/credentials.js +32 -0
- package/db-tools.d.ts +2 -2
- package/db-tools.js +14 -18
- package/esm/account-store.d.ts +19 -0
- package/esm/account-store.js +54 -0
- package/esm/api-key.d.ts +51 -0
- package/esm/api-key.js +88 -0
- package/esm/auth-error.d.ts +4 -0
- package/esm/auth-error.js +108 -0
- package/esm/auth.d.ts +28 -0
- package/esm/auth.js +114 -0
- package/esm/backend-store.d.ts +19 -0
- package/esm/backend-store.js +58 -0
- package/esm/commands.d.ts +3 -0
- package/esm/commands.js +109 -0
- package/esm/config.d.ts +16 -0
- package/esm/config.js +51 -0
- package/esm/credentials.d.ts +11 -0
- package/esm/credentials.js +28 -0
- package/esm/db-tools.d.ts +2 -2
- package/esm/db-tools.js +14 -18
- package/esm/index.js +24 -1
- package/index.js +23 -0
- package/package.json +7 -5
package/commands.js
CHANGED
|
@@ -3,8 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.skillsList = skillsList;
|
|
4
4
|
exports.skillsUpdate = skillsUpdate;
|
|
5
5
|
exports.init = init;
|
|
6
|
+
exports.login = login;
|
|
7
|
+
exports.logout = logout;
|
|
8
|
+
exports.whoami = whoami;
|
|
6
9
|
exports.usage = usage;
|
|
10
|
+
const pi_1 = require("@agentic-kit/pi");
|
|
7
11
|
const inquirerer_1 = require("inquirerer");
|
|
12
|
+
const account_store_1 = require("./account-store");
|
|
13
|
+
const auth_1 = require("./auth");
|
|
14
|
+
const backend_store_1 = require("./backend-store");
|
|
8
15
|
const config_1 = require("./config");
|
|
9
16
|
const skills_1 = require("./skills");
|
|
10
17
|
const log = (msg) => console.log(`[agent] ${msg}`);
|
|
@@ -45,6 +52,108 @@ async function init(config, argv) {
|
|
|
45
52
|
log(`wrote ${config.manifestFile}`);
|
|
46
53
|
log(`local overlay dir (highest precedence): ${config.overlayDir}`);
|
|
47
54
|
}
|
|
55
|
+
function maskKey(token) {
|
|
56
|
+
if (token.length <= 10)
|
|
57
|
+
return '****';
|
|
58
|
+
return `${token.slice(0, 6)}...${token.slice(-4)}`;
|
|
59
|
+
}
|
|
60
|
+
async function login(config, argv) {
|
|
61
|
+
if (!process.stdin.isTTY && !(argv.email && argv.password)) {
|
|
62
|
+
throw new Error('agent login is interactive and needs a terminal. For headless use, set CONSTRUCTIVE_USER_ID, CONSTRUCTIVE_ACCESS_TOKEN, and CONSTRUCTIVE_API_KEY.');
|
|
63
|
+
}
|
|
64
|
+
const saved = (0, backend_store_1.loadBackendConfig)(config.store);
|
|
65
|
+
const prompter = new inquirerer_1.Inquirerer({ noTty: !process.stdin.isTTY });
|
|
66
|
+
try {
|
|
67
|
+
const answers = await prompter.prompt(argv, [
|
|
68
|
+
{
|
|
69
|
+
type: 'list',
|
|
70
|
+
name: 'backend',
|
|
71
|
+
message: 'Backend',
|
|
72
|
+
options: [...Object.keys(backend_store_1.BACKEND_PRESETS), 'custom'],
|
|
73
|
+
default: saved ? (0, backend_store_1.contextNameFor)(saved) : 'localnet'
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
type: 'text',
|
|
77
|
+
name: 'apiUrl',
|
|
78
|
+
message: 'API GraphQL endpoint (e.g. https://api.example.com/graphql)',
|
|
79
|
+
default: saved?.apiEndpoint,
|
|
80
|
+
when: (a) => a.backend === 'custom'
|
|
81
|
+
},
|
|
82
|
+
{ type: 'text', name: 'email', message: 'Email' },
|
|
83
|
+
{ type: 'password', name: 'password', message: 'Password' }
|
|
84
|
+
]);
|
|
85
|
+
let backend;
|
|
86
|
+
if (answers.backend === 'custom') {
|
|
87
|
+
const apiUrl = String(answers.apiUrl ?? '').trim();
|
|
88
|
+
const authEndpoint = (0, pi_1.deriveSubdomainEndpoint)(apiUrl, 'auth');
|
|
89
|
+
const modulesEndpoint = (0, pi_1.deriveSubdomainEndpoint)(apiUrl, 'modules');
|
|
90
|
+
if (!apiUrl || !authEndpoint || !modulesEndpoint) {
|
|
91
|
+
throw new Error(`Invalid API endpoint URL: ${apiUrl || '(empty)'}`);
|
|
92
|
+
}
|
|
93
|
+
backend = { apiEndpoint: apiUrl, authEndpoint, modulesEndpoint };
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
backend = backend_store_1.BACKEND_PRESETS[String(answers.backend)];
|
|
97
|
+
if (!backend)
|
|
98
|
+
throw new Error(`Unknown backend preset: ${answers.backend}`);
|
|
99
|
+
}
|
|
100
|
+
// File the session under the backend's context name, then commit the backend
|
|
101
|
+
// itself: a failed sign-in must leave neither behind.
|
|
102
|
+
const contextName = (0, backend_store_1.contextNameFor)(backend);
|
|
103
|
+
const session = await (0, auth_1.signIn)({
|
|
104
|
+
store: config.store,
|
|
105
|
+
context: contextName,
|
|
106
|
+
authEndpoint: backend.authEndpoint,
|
|
107
|
+
email: String(answers.email ?? ''),
|
|
108
|
+
password: String(answers.password ?? '')
|
|
109
|
+
});
|
|
110
|
+
(0, backend_store_1.saveBackendConfig)(config.store, backend);
|
|
111
|
+
log(`signed in as ${session.email}`);
|
|
112
|
+
log(`backend: ${backend.apiEndpoint}`);
|
|
113
|
+
if (session.apiKey) {
|
|
114
|
+
log(`API key ${maskKey(session.apiKey)} (expires ${session.apiKeyExpiresAt ?? 'unknown'})`);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
log('warning: API key mint failed — db tools stay signed out. Run `agent login` again to retry.');
|
|
118
|
+
}
|
|
119
|
+
log(`session stored in context ${contextName}`);
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
prompter.close();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function logout(config) {
|
|
126
|
+
const backend = (0, backend_store_1.loadBackendConfig)(config.store) ?? backend_store_1.BACKEND_PRESETS.localnet;
|
|
127
|
+
const wasSignedIn = await (0, auth_1.signOut)({
|
|
128
|
+
store: config.store,
|
|
129
|
+
authEndpoint: backend.authEndpoint
|
|
130
|
+
});
|
|
131
|
+
if (wasSignedIn)
|
|
132
|
+
log('signed out — API key revoked and session cleared.');
|
|
133
|
+
else
|
|
134
|
+
log('not signed in.');
|
|
135
|
+
}
|
|
136
|
+
function whoami(config) {
|
|
137
|
+
const session = (0, account_store_1.loadSession)(config.store);
|
|
138
|
+
if (!session) {
|
|
139
|
+
log('not signed in — run `agent login`');
|
|
140
|
+
process.exitCode = 1;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const backend = (0, backend_store_1.loadBackendConfig)(config.store);
|
|
144
|
+
log(`signed in as ${session.email}`);
|
|
145
|
+
log(`user id: ${session.userId}`);
|
|
146
|
+
log(`backend: ${backend?.apiEndpoint ?? 'unknown'}`);
|
|
147
|
+
if (session.apiKey) {
|
|
148
|
+
log(`API key: ${maskKey(session.apiKey)} (expires ${session.apiKeyExpiresAt ?? 'unknown'})`);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
log('API key: none — db tools stay signed out. Run `agent login` to mint one.');
|
|
152
|
+
}
|
|
153
|
+
if (session.accessTokenExpiresAt)
|
|
154
|
+
log(`access token expires: ${session.accessTokenExpiresAt}`);
|
|
155
|
+
log(`context: ${config.store.getCurrentContext()?.name ?? 'none'}`);
|
|
156
|
+
}
|
|
48
157
|
function usage() {
|
|
49
158
|
console.log(`agent — the pi coding agent with the Constructive harness baked in
|
|
50
159
|
|
|
@@ -52,6 +161,9 @@ Usage:
|
|
|
52
161
|
agent [pi options...] start an interactive session (pi TUI)
|
|
53
162
|
agent -p "prompt" one-shot print mode (pi)
|
|
54
163
|
agent init configure the skills source (repo + pin)
|
|
164
|
+
agent login sign in to the Constructive platform
|
|
165
|
+
agent logout revoke the API key and clear the session
|
|
166
|
+
agent whoami show the signed-in account
|
|
55
167
|
agent skills list resolve + list the effective skill set
|
|
56
168
|
agent skills update re-fetch the base release and re-materialize
|
|
57
169
|
agent help show this help
|
package/config.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { HarnessDirs, SkillsManifest } from '@agentic-kit/harness';
|
|
2
|
+
import { ConfigStore } from 'appstash';
|
|
2
3
|
export declare const DEFAULT_SKILLS_REPO = "constructive-io/constructive-skills";
|
|
4
|
+
/**
|
|
5
|
+
* Directory identity shared with the csdk CLI and the desktop app, so a sign-in
|
|
6
|
+
* here is a sign-in there. `agent` is only the tool's own name (env prefixes,
|
|
7
|
+
* help text).
|
|
8
|
+
*/
|
|
9
|
+
export declare const STASH_NAME = "constructive";
|
|
10
|
+
export declare const TOOL_NAME = "agent";
|
|
3
11
|
export declare const DEFAULT_SKILLS_PIN = "main";
|
|
4
12
|
/** Layer names understood by the assembler. */
|
|
5
13
|
export declare const BASE_LAYER = "constructive-skills";
|
|
@@ -12,6 +20,8 @@ export interface AgentCliConfig {
|
|
|
12
20
|
overlayDir: string;
|
|
13
21
|
/** Path of the user-editable manifest: `<config>/skills-manifest.json`. */
|
|
14
22
|
manifestFile: string;
|
|
23
|
+
/** Shared contexts + credentials store (endpoints and signed-in session). */
|
|
24
|
+
store: ConfigStore;
|
|
15
25
|
manifest: SkillsManifest;
|
|
16
26
|
skillsRepo: string;
|
|
17
27
|
skillsPin: string;
|
|
@@ -22,6 +32,12 @@ interface ManifestFile {
|
|
|
22
32
|
manifest?: SkillsManifest;
|
|
23
33
|
}
|
|
24
34
|
export declare function defaultManifest(): SkillsManifest;
|
|
35
|
+
/**
|
|
36
|
+
* Move a pre-shared-store sign-in (`agent/account.json` +
|
|
37
|
+
* `agent/backend-config.json`) into the store, once. The originals are renamed
|
|
38
|
+
* rather than deleted, so a downgrade still finds them.
|
|
39
|
+
*/
|
|
40
|
+
export declare function importLegacyAgentFiles(store: ConfigStore, legacyDir: string): void;
|
|
25
41
|
export declare function loadConfig(baseDir?: string): AgentCliConfig;
|
|
26
42
|
export declare function saveManifestFile(config: AgentCliConfig, file: ManifestFile): void;
|
|
27
43
|
export {};
|
package/config.js
CHANGED
|
@@ -33,14 +33,25 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.OVERLAY_LAYER = exports.BASE_LAYER = exports.DEFAULT_SKILLS_PIN = exports.DEFAULT_SKILLS_REPO = void 0;
|
|
36
|
+
exports.OVERLAY_LAYER = exports.BASE_LAYER = exports.DEFAULT_SKILLS_PIN = exports.TOOL_NAME = exports.STASH_NAME = exports.DEFAULT_SKILLS_REPO = void 0;
|
|
37
37
|
exports.defaultManifest = defaultManifest;
|
|
38
|
+
exports.importLegacyAgentFiles = importLegacyAgentFiles;
|
|
38
39
|
exports.loadConfig = loadConfig;
|
|
39
40
|
exports.saveManifestFile = saveManifestFile;
|
|
40
41
|
const harness_1 = require("@agentic-kit/harness");
|
|
42
|
+
const appstash_1 = require("appstash");
|
|
41
43
|
const fs = __importStar(require("fs"));
|
|
42
44
|
const path = __importStar(require("path"));
|
|
45
|
+
const account_store_1 = require("./account-store");
|
|
46
|
+
const backend_store_1 = require("./backend-store");
|
|
43
47
|
exports.DEFAULT_SKILLS_REPO = 'constructive-io/constructive-skills';
|
|
48
|
+
/**
|
|
49
|
+
* Directory identity shared with the csdk CLI and the desktop app, so a sign-in
|
|
50
|
+
* here is a sign-in there. `agent` is only the tool's own name (env prefixes,
|
|
51
|
+
* help text).
|
|
52
|
+
*/
|
|
53
|
+
exports.STASH_NAME = 'constructive';
|
|
54
|
+
exports.TOOL_NAME = 'agent';
|
|
44
55
|
exports.DEFAULT_SKILLS_PIN = 'main';
|
|
45
56
|
/** Layer names understood by the assembler. */
|
|
46
57
|
exports.BASE_LAYER = 'constructive-skills';
|
|
@@ -50,13 +61,53 @@ function defaultManifest() {
|
|
|
50
61
|
sources: [{ name: exports.BASE_LAYER }, { name: exports.OVERLAY_LAYER }]
|
|
51
62
|
};
|
|
52
63
|
}
|
|
64
|
+
function readLegacyFile(file) {
|
|
65
|
+
if (!fs.existsSync(file))
|
|
66
|
+
return null;
|
|
67
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Move a pre-shared-store sign-in (`agent/account.json` +
|
|
71
|
+
* `agent/backend-config.json`) into the store, once. The originals are renamed
|
|
72
|
+
* rather than deleted, so a downgrade still finds them.
|
|
73
|
+
*/
|
|
74
|
+
function importLegacyAgentFiles(store, legacyDir) {
|
|
75
|
+
const accountFile = path.join(legacyDir, 'account.json');
|
|
76
|
+
const backendFile = path.join(legacyDir, 'backend-config.json');
|
|
77
|
+
if (!fs.existsSync(accountFile) && !fs.existsSync(backendFile))
|
|
78
|
+
return;
|
|
79
|
+
const backend = readLegacyFile(backendFile);
|
|
80
|
+
if (backend?.apiEndpoint && backend.authEndpoint && backend.modulesEndpoint) {
|
|
81
|
+
(0, backend_store_1.saveBackendConfig)(store, backend);
|
|
82
|
+
}
|
|
83
|
+
const legacy = readLegacyFile(accountFile);
|
|
84
|
+
if (legacy?.token && legacy.userId) {
|
|
85
|
+
const session = {
|
|
86
|
+
userId: legacy.userId,
|
|
87
|
+
email: legacy.email ?? '',
|
|
88
|
+
accessToken: legacy.token,
|
|
89
|
+
accessTokenExpiresAt: legacy.accessTokenExpiresAt,
|
|
90
|
+
apiKey: legacy.apiKey,
|
|
91
|
+
keyId: legacy.keyId,
|
|
92
|
+
apiKeyExpiresAt: legacy.apiKeyExpiresAt,
|
|
93
|
+
signedInAt: legacy.signedInAt ?? Date.now()
|
|
94
|
+
};
|
|
95
|
+
(0, account_store_1.saveSession)(store, session);
|
|
96
|
+
}
|
|
97
|
+
for (const file of [accountFile, backendFile]) {
|
|
98
|
+
if (fs.existsSync(file))
|
|
99
|
+
fs.renameSync(file, `${file}.migrated`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
53
102
|
function loadConfig(baseDir) {
|
|
54
103
|
const dirs = (0, harness_1.harnessDirs)('constructive', baseDir);
|
|
55
104
|
const agentDir = path.join(dirs.stash.data, 'agent');
|
|
56
105
|
const overlayDir = path.join(dirs.stash.config, 'skills-overlay');
|
|
57
106
|
const manifestFile = path.join(dirs.stash.config, 'skills-manifest.json');
|
|
107
|
+
const store = (0, appstash_1.createConfigStore)(exports.TOOL_NAME, { stashName: exports.STASH_NAME, baseDir });
|
|
58
108
|
fs.mkdirSync(agentDir, { recursive: true });
|
|
59
109
|
fs.mkdirSync(overlayDir, { recursive: true });
|
|
110
|
+
importLegacyAgentFiles(store, path.join(dirs.stash.config, 'agent'));
|
|
60
111
|
let file = {};
|
|
61
112
|
if (fs.existsSync(manifestFile)) {
|
|
62
113
|
file = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
|
|
@@ -66,6 +117,7 @@ function loadConfig(baseDir) {
|
|
|
66
117
|
agentDir,
|
|
67
118
|
overlayDir,
|
|
68
119
|
manifestFile,
|
|
120
|
+
store,
|
|
69
121
|
manifest: file.manifest ?? defaultManifest(),
|
|
70
122
|
skillsRepo: process.env.AGENT_SKILLS_REPO ?? file.repo ?? exports.DEFAULT_SKILLS_REPO,
|
|
71
123
|
skillsPin: process.env.AGENT_SKILLS_PIN ?? file.pin ?? exports.DEFAULT_SKILLS_PIN
|
package/credentials.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type ResolvedAccount = {
|
|
2
|
+
userId: string;
|
|
3
|
+
accessToken: string;
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
};
|
|
6
|
+
export type ResolvedBackendConfig = {
|
|
7
|
+
apiEndpoint?: string;
|
|
8
|
+
modulesEndpoint?: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function resolveAccount(): ResolvedAccount | null;
|
|
11
|
+
export declare function resolveBackendConfig(): ResolvedBackendConfig | undefined;
|
package/credentials.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveAccount = resolveAccount;
|
|
4
|
+
exports.resolveBackendConfig = resolveBackendConfig;
|
|
5
|
+
const account_store_1 = require("./account-store");
|
|
6
|
+
const backend_store_1 = require("./backend-store");
|
|
7
|
+
const config_1 = require("./config");
|
|
8
|
+
// Both resolvers run on every tool call and re-read the store, so a login or
|
|
9
|
+
// logout mid-session is picked up without a restart. CONSTRUCTIVE_* env vars
|
|
10
|
+
// win over the store (the CI/headless path); tokens never enter process.env
|
|
11
|
+
// from here.
|
|
12
|
+
function resolveAccount() {
|
|
13
|
+
const userId = process.env.CONSTRUCTIVE_USER_ID;
|
|
14
|
+
const accessToken = process.env.CONSTRUCTIVE_ACCESS_TOKEN;
|
|
15
|
+
if (userId && accessToken) {
|
|
16
|
+
return { userId, accessToken, apiKey: process.env.CONSTRUCTIVE_API_KEY };
|
|
17
|
+
}
|
|
18
|
+
const session = (0, account_store_1.loadSession)((0, config_1.loadConfig)(process.env.AGENT_HOME).store);
|
|
19
|
+
if (!session)
|
|
20
|
+
return null;
|
|
21
|
+
return { userId: session.userId, accessToken: session.accessToken, apiKey: session.apiKey };
|
|
22
|
+
}
|
|
23
|
+
function resolveBackendConfig() {
|
|
24
|
+
const apiEndpoint = process.env.CONSTRUCTIVE_API_ENDPOINT;
|
|
25
|
+
const modulesEndpoint = process.env.CONSTRUCTIVE_MODULES_ENDPOINT;
|
|
26
|
+
if (apiEndpoint || modulesEndpoint)
|
|
27
|
+
return { apiEndpoint, modulesEndpoint };
|
|
28
|
+
const stored = (0, backend_store_1.loadBackendConfig)((0, config_1.loadConfig)(process.env.AGENT_HOME).store);
|
|
29
|
+
if (!stored)
|
|
30
|
+
return undefined;
|
|
31
|
+
return { apiEndpoint: stored.apiEndpoint, modulesEndpoint: stored.modulesEndpoint };
|
|
32
|
+
}
|
package/db-tools.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AgentCliConfig } from './config';
|
|
2
2
|
/**
|
|
3
|
-
* Environment variables the
|
|
4
|
-
* Without
|
|
3
|
+
* Environment variables that override the stored session at tool-call time.
|
|
4
|
+
* Without env vars or a stored session the tools load but report "not signed in".
|
|
5
5
|
*/
|
|
6
6
|
export declare const HOST_ENV_VARS: readonly ["CONSTRUCTIVE_USER_ID", "CONSTRUCTIVE_ACCESS_TOKEN", "CONSTRUCTIVE_API_KEY", "CONSTRUCTIVE_API_ENDPOINT", "CONSTRUCTIVE_MODULES_ENDPOINT"];
|
|
7
7
|
/**
|
package/db-tools.js
CHANGED
|
@@ -39,8 +39,8 @@ const fs = __importStar(require("fs"));
|
|
|
39
39
|
const path = __importStar(require("path"));
|
|
40
40
|
const EXTENSION_FILE = 'constructive-db-tools.js';
|
|
41
41
|
/**
|
|
42
|
-
* Environment variables the
|
|
43
|
-
* Without
|
|
42
|
+
* Environment variables that override the stored session at tool-call time.
|
|
43
|
+
* Without env vars or a stored session the tools load but report "not signed in".
|
|
44
44
|
*/
|
|
45
45
|
exports.HOST_ENV_VARS = [
|
|
46
46
|
'CONSTRUCTIVE_USER_ID',
|
|
@@ -51,26 +51,21 @@ exports.HOST_ENV_VARS = [
|
|
|
51
51
|
];
|
|
52
52
|
// pi's TUI loads extensions from files under <agentDir>/extensions (via jiti),
|
|
53
53
|
// not from factories, so the db tools are wired by materializing a small
|
|
54
|
-
// generated entry there.
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
function extensionSource(pkgEntry) {
|
|
54
|
+
// generated entry there. Both entry paths are resolved from THIS package at
|
|
55
|
+
// assembly time and baked in, because node resolution from the appstash agent
|
|
56
|
+
// dir would not find them.
|
|
57
|
+
function extensionSource(pkgEntry, credentialsEntry) {
|
|
58
58
|
return `// Generated by @agentic-kit/cli — do not edit (rewritten on every \`agent\` run).
|
|
59
59
|
// Registers the Constructive typed db tools + confirm gate (@agentic-kit/pi)
|
|
60
|
-
// with credentials
|
|
60
|
+
// with credentials resolved on each call: CONSTRUCTIVE_* env vars first, then
|
|
61
|
+
// the \`agent login\` session store.
|
|
61
62
|
const { createDbTools } = require(${JSON.stringify(pkgEntry)});
|
|
63
|
+
const { resolveAccount, resolveBackendConfig } = require(${JSON.stringify(credentialsEntry)});
|
|
62
64
|
|
|
63
65
|
module.exports = createDbTools({
|
|
64
|
-
account:
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (!userId || !accessToken) return null;
|
|
68
|
-
return { userId, accessToken, apiKey: process.env.CONSTRUCTIVE_API_KEY };
|
|
69
|
-
},
|
|
70
|
-
backendConfig: () => ({
|
|
71
|
-
apiEndpoint: process.env.CONSTRUCTIVE_API_ENDPOINT,
|
|
72
|
-
modulesEndpoint: process.env.CONSTRUCTIVE_MODULES_ENDPOINT
|
|
73
|
-
})
|
|
66
|
+
account: resolveAccount,
|
|
67
|
+
backendConfig: resolveBackendConfig,
|
|
68
|
+
signInHint: 'Run \`agent login\` to sign in.'
|
|
74
69
|
});
|
|
75
70
|
`;
|
|
76
71
|
}
|
|
@@ -87,9 +82,10 @@ function materializeDbTools(config, log) {
|
|
|
87
82
|
log('db tools unavailable (@agentic-kit/pi not installed) — continuing without them');
|
|
88
83
|
return null;
|
|
89
84
|
}
|
|
85
|
+
const credentialsEntry = require.resolve('./credentials');
|
|
90
86
|
const dir = path.join(config.agentDir, 'extensions');
|
|
91
87
|
fs.mkdirSync(dir, { recursive: true });
|
|
92
88
|
const file = path.join(dir, EXTENSION_FILE);
|
|
93
|
-
fs.writeFileSync(file, extensionSource(pkgEntry));
|
|
89
|
+
fs.writeFileSync(file, extensionSource(pkgEntry, credentialsEntry));
|
|
94
90
|
return file;
|
|
95
91
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ConfigStore } from 'appstash';
|
|
2
|
+
export interface AccountSession {
|
|
3
|
+
userId: string;
|
|
4
|
+
email: string;
|
|
5
|
+
accessToken: string;
|
|
6
|
+
accessTokenExpiresAt?: string;
|
|
7
|
+
apiKey?: string;
|
|
8
|
+
keyId?: string;
|
|
9
|
+
apiKeyExpiresAt?: string;
|
|
10
|
+
signedInAt: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function loadSession(store: ConfigStore, context?: string): AccountSession | null;
|
|
13
|
+
/**
|
|
14
|
+
* `context` names the backend the session belongs to; it does not have to exist
|
|
15
|
+
* as a context yet, so a sign-in can be filed before the backend is committed
|
|
16
|
+
* and a failed sign-in leaves nothing behind.
|
|
17
|
+
*/
|
|
18
|
+
export declare function saveSession(store: ConfigStore, session: AccountSession, context?: string): void;
|
|
19
|
+
export declare function clearSession(store: ConfigStore, context?: string): void;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { BACKEND_PRESETS, saveBackendConfig } from './backend-store';
|
|
2
|
+
/**
|
|
3
|
+
* Sessions live in the shared Constructive stash, as the credentials of the
|
|
4
|
+
* active context (the chosen backend). The store owns the file layout, the
|
|
5
|
+
* atomic 0600 writes and the at-rest encoding; this module only maps between
|
|
6
|
+
* its `ContextCredentials` and the session shape the CLI passes around.
|
|
7
|
+
*/
|
|
8
|
+
function currentContextName(store) {
|
|
9
|
+
return store.getCurrentContext()?.name ?? null;
|
|
10
|
+
}
|
|
11
|
+
/** The active context, defaulting to localnet the first time one is needed. */
|
|
12
|
+
function ensureContextName(store) {
|
|
13
|
+
return currentContextName(store) ?? saveBackendConfig(store, BACKEND_PRESETS.localnet);
|
|
14
|
+
}
|
|
15
|
+
export function loadSession(store, context) {
|
|
16
|
+
const contextName = context ?? currentContextName(store);
|
|
17
|
+
if (!contextName)
|
|
18
|
+
return null;
|
|
19
|
+
const creds = store.getCredentials(contextName);
|
|
20
|
+
if (!creds?.token || !creds.userId)
|
|
21
|
+
return null;
|
|
22
|
+
return {
|
|
23
|
+
userId: creds.userId,
|
|
24
|
+
email: creds.email ?? '',
|
|
25
|
+
accessToken: creds.token,
|
|
26
|
+
accessTokenExpiresAt: creds.expiresAt,
|
|
27
|
+
apiKey: creds.apiKey,
|
|
28
|
+
keyId: creds.keyId,
|
|
29
|
+
apiKeyExpiresAt: creds.apiKeyExpiresAt,
|
|
30
|
+
signedInAt: creds.signedInAt ?? 0
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* `context` names the backend the session belongs to; it does not have to exist
|
|
35
|
+
* as a context yet, so a sign-in can be filed before the backend is committed
|
|
36
|
+
* and a failed sign-in leaves nothing behind.
|
|
37
|
+
*/
|
|
38
|
+
export function saveSession(store, session, context) {
|
|
39
|
+
store.setCredentials(context ?? ensureContextName(store), {
|
|
40
|
+
token: session.accessToken,
|
|
41
|
+
expiresAt: session.accessTokenExpiresAt,
|
|
42
|
+
userId: session.userId,
|
|
43
|
+
email: session.email,
|
|
44
|
+
apiKey: session.apiKey,
|
|
45
|
+
keyId: session.keyId,
|
|
46
|
+
apiKeyExpiresAt: session.apiKeyExpiresAt,
|
|
47
|
+
signedInAt: session.signedInAt
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export function clearSession(store, context) {
|
|
51
|
+
const contextName = context ?? currentContextName(store);
|
|
52
|
+
if (contextName)
|
|
53
|
+
store.removeCredentials(contextName);
|
|
54
|
+
}
|
package/esm/api-key.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export declare const API_KEY_NAME = "agentic-cli";
|
|
2
|
+
export declare const API_KEY_ACCESS_LEVEL = "full_access";
|
|
3
|
+
export declare const API_KEY_MFA_LEVEL = "none";
|
|
4
|
+
export declare const API_KEY_YEARS = 5;
|
|
5
|
+
/**
|
|
6
|
+
* Re-mint once the key is within a week of expiry. The mint needs a
|
|
7
|
+
* fresh-password step-up window, so in practice the refresh succeeds only right
|
|
8
|
+
* after a sign-in; otherwise it surfaces as a re-auth prompt.
|
|
9
|
+
*/
|
|
10
|
+
export declare const REMINT_THRESHOLD_MS: number;
|
|
11
|
+
export interface MintedApiKey {
|
|
12
|
+
apiKey: string;
|
|
13
|
+
keyId: string;
|
|
14
|
+
apiKeyExpiresAt?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface CreateApiKeyInputShape {
|
|
17
|
+
keyName: string;
|
|
18
|
+
accessLevel: string;
|
|
19
|
+
mfaLevel: string;
|
|
20
|
+
expiresIn: {
|
|
21
|
+
years: number;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export declare function buildCreateApiKeyInput({ years }: {
|
|
25
|
+
years: number;
|
|
26
|
+
}): CreateApiKeyInputShape;
|
|
27
|
+
interface RawApiKeyRecord {
|
|
28
|
+
apiKey?: string | null;
|
|
29
|
+
keyId?: string | null;
|
|
30
|
+
expiresAt?: string | null;
|
|
31
|
+
}
|
|
32
|
+
export declare function parseMintedKey(record: RawApiKeyRecord | null | undefined): MintedApiKey;
|
|
33
|
+
export type ApiKeyErrorKind = 'step-up-required' | 'disabled' | 'limit-reached' | 'not-authenticated' | 'invalid-access-level' | 'unknown';
|
|
34
|
+
export declare function classifyApiKeyError(err: unknown): ApiKeyErrorKind;
|
|
35
|
+
export declare function needsRemint({ apiKeyExpiresAt, now, thresholdMs }: {
|
|
36
|
+
apiKeyExpiresAt: string | undefined;
|
|
37
|
+
now: number;
|
|
38
|
+
thresholdMs?: number;
|
|
39
|
+
}): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Mint-first, revoke-on-success: revoking before minting would destroy a
|
|
42
|
+
* still-valid key whenever the mint then fails, leaving no credential at all.
|
|
43
|
+
* The one case that needs the old key gone first is the live-key cap
|
|
44
|
+
* (API_KEY_LIMIT_REACHED): there we revoke, then retry the mint once.
|
|
45
|
+
*/
|
|
46
|
+
export declare function remintApiKey({ currentKeyId, revoke, mint }: {
|
|
47
|
+
currentKeyId: string | undefined;
|
|
48
|
+
revoke: (keyId: string) => Promise<void>;
|
|
49
|
+
mint: () => Promise<MintedApiKey>;
|
|
50
|
+
}): Promise<MintedApiKey>;
|
|
51
|
+
export {};
|
package/esm/api-key.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export const API_KEY_NAME = 'agentic-cli';
|
|
2
|
+
export const API_KEY_ACCESS_LEVEL = 'full_access';
|
|
3
|
+
export const API_KEY_MFA_LEVEL = 'none';
|
|
4
|
+
export const API_KEY_YEARS = 5;
|
|
5
|
+
/**
|
|
6
|
+
* Re-mint once the key is within a week of expiry. The mint needs a
|
|
7
|
+
* fresh-password step-up window, so in practice the refresh succeeds only right
|
|
8
|
+
* after a sign-in; otherwise it surfaces as a re-auth prompt.
|
|
9
|
+
*/
|
|
10
|
+
export const REMINT_THRESHOLD_MS = 1000 * 60 * 60 * 24 * 7;
|
|
11
|
+
export function buildCreateApiKeyInput({ years }) {
|
|
12
|
+
return {
|
|
13
|
+
keyName: API_KEY_NAME,
|
|
14
|
+
accessLevel: API_KEY_ACCESS_LEVEL,
|
|
15
|
+
mfaLevel: API_KEY_MFA_LEVEL,
|
|
16
|
+
expiresIn: { years }
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function parseMintedKey(record) {
|
|
20
|
+
if (!record?.apiKey || !record.keyId) {
|
|
21
|
+
throw new Error('createApiKey returned no API key.');
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
apiKey: record.apiKey,
|
|
25
|
+
keyId: record.keyId,
|
|
26
|
+
...(record.expiresAt ? { apiKeyExpiresAt: record.expiresAt } : {})
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const ERROR_CODE_MAP = {
|
|
30
|
+
STEP_UP_REQUIRED: 'step-up-required',
|
|
31
|
+
API_KEYS_DISABLED: 'disabled',
|
|
32
|
+
API_KEY_LIMIT_REACHED: 'limit-reached',
|
|
33
|
+
NOT_AUTHENTICATED: 'not-authenticated',
|
|
34
|
+
INVALID_ACCESS_LEVEL: 'invalid-access-level'
|
|
35
|
+
};
|
|
36
|
+
export function classifyApiKeyError(err) {
|
|
37
|
+
const errors = err?.errors;
|
|
38
|
+
if (Array.isArray(errors)) {
|
|
39
|
+
for (const e of errors) {
|
|
40
|
+
const code = e.extensions?.code;
|
|
41
|
+
if (code && ERROR_CODE_MAP[code])
|
|
42
|
+
return ERROR_CODE_MAP[code];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const message = err instanceof Error ? err.message : typeof err === 'string' ? err : '';
|
|
46
|
+
for (const code of Object.keys(ERROR_CODE_MAP)) {
|
|
47
|
+
if (message.includes(code))
|
|
48
|
+
return ERROR_CODE_MAP[code];
|
|
49
|
+
}
|
|
50
|
+
return 'unknown';
|
|
51
|
+
}
|
|
52
|
+
export function needsRemint({ apiKeyExpiresAt, now, thresholdMs = REMINT_THRESHOLD_MS }) {
|
|
53
|
+
if (!apiKeyExpiresAt)
|
|
54
|
+
return false;
|
|
55
|
+
const expires = Date.parse(apiKeyExpiresAt);
|
|
56
|
+
if (Number.isNaN(expires))
|
|
57
|
+
return false;
|
|
58
|
+
return expires - now <= thresholdMs;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Mint-first, revoke-on-success: revoking before minting would destroy a
|
|
62
|
+
* still-valid key whenever the mint then fails, leaving no credential at all.
|
|
63
|
+
* The one case that needs the old key gone first is the live-key cap
|
|
64
|
+
* (API_KEY_LIMIT_REACHED): there we revoke, then retry the mint once.
|
|
65
|
+
*/
|
|
66
|
+
export async function remintApiKey({ currentKeyId, revoke, mint }) {
|
|
67
|
+
const revokeOld = async () => {
|
|
68
|
+
if (!currentKeyId)
|
|
69
|
+
return;
|
|
70
|
+
try {
|
|
71
|
+
await revoke(currentKeyId);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
console.warn(`[agent] failed to revoke superseded API key ${currentKeyId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
try {
|
|
78
|
+
const minted = await mint();
|
|
79
|
+
await revokeOld();
|
|
80
|
+
return minted;
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
if (classifyApiKeyError(err) !== 'limit-reached')
|
|
84
|
+
throw err;
|
|
85
|
+
await revokeOld();
|
|
86
|
+
return mint();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function isNetworkError(err: unknown): boolean;
|
|
2
|
+
export declare function describeAuthError(err: unknown, endpoint: string): string;
|
|
3
|
+
export declare const AUTH_TIMEOUT_MS = 10000;
|
|
4
|
+
export declare function withAuthTimeout<T>(promise: Promise<T>, endpoint: string, ms?: number): Promise<T>;
|