trucontext 0.1.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/LICENSE +21 -0
- package/PRIVACY.md +119 -0
- package/README.md +183 -0
- package/TERMS.md +101 -0
- package/bin/cli.js +157 -0
- package/package.json +49 -0
- package/src/auth.js +197 -0
- package/src/client.js +77 -0
- package/src/commands/apps.js +48 -0
- package/src/commands/contexts.js +56 -0
- package/src/commands/entities.js +141 -0
- package/src/commands/ingest.js +59 -0
- package/src/commands/init.js +81 -0
- package/src/commands/login.js +126 -0
- package/src/commands/logout.js +7 -0
- package/src/commands/query.js +51 -0
- package/src/commands/recall.js +42 -0
- package/src/commands/recipes.js +130 -0
- package/src/commands/relationship-types.js +56 -0
- package/src/commands/schema.js +74 -0
- package/src/commands/whoami.js +23 -0
- package/src/config.js +87 -0
package/src/auth.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// auth.js — PKCE Authorization Code flow for CLI login
|
|
2
|
+
|
|
3
|
+
import { createHash, randomBytes } from 'crypto';
|
|
4
|
+
import { createServer } from 'http';
|
|
5
|
+
import open from 'open';
|
|
6
|
+
import {
|
|
7
|
+
COGNITO_DOMAIN,
|
|
8
|
+
CLI_CLIENT_ID,
|
|
9
|
+
CALLBACK_PORT,
|
|
10
|
+
CALLBACK_URL,
|
|
11
|
+
getCredentials,
|
|
12
|
+
saveCredentials,
|
|
13
|
+
} from './config.js';
|
|
14
|
+
|
|
15
|
+
function base64url(buffer) {
|
|
16
|
+
return buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function generateCodeVerifier() {
|
|
20
|
+
return base64url(randomBytes(32));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function generateCodeChallenge(verifier) {
|
|
24
|
+
return base64url(createHash('sha256').update(verifier).digest());
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Run the PKCE login flow:
|
|
29
|
+
* 1. Start localhost server
|
|
30
|
+
* 2. Open browser to Cognito authorize endpoint
|
|
31
|
+
* 3. Capture auth code from redirect
|
|
32
|
+
* 4. Exchange for tokens
|
|
33
|
+
* 5. Save credentials
|
|
34
|
+
*/
|
|
35
|
+
export async function performLogin({ provider } = {}) {
|
|
36
|
+
const codeVerifier = generateCodeVerifier();
|
|
37
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
38
|
+
const state = base64url(randomBytes(16));
|
|
39
|
+
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const server = createServer(async (req, res) => {
|
|
42
|
+
const url = new URL(req.url, `http://localhost:${CALLBACK_PORT}`);
|
|
43
|
+
|
|
44
|
+
if (url.pathname !== '/callback') {
|
|
45
|
+
res.writeHead(404);
|
|
46
|
+
res.end('Not found');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const code = url.searchParams.get('code');
|
|
51
|
+
const returnedState = url.searchParams.get('state');
|
|
52
|
+
const error = url.searchParams.get('error');
|
|
53
|
+
|
|
54
|
+
if (error) {
|
|
55
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
56
|
+
res.end('<html><body><h2>Login failed</h2><p>You can close this window.</p></body></html>');
|
|
57
|
+
server.close();
|
|
58
|
+
reject(new Error(`Auth error: ${error}`));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (returnedState !== state) {
|
|
63
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
64
|
+
res.end('<html><body><h2>Invalid state</h2><p>Please try again.</p></body></html>');
|
|
65
|
+
server.close();
|
|
66
|
+
reject(new Error('State mismatch — possible CSRF attack'));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
// Exchange auth code for tokens
|
|
72
|
+
const tokenRes = await fetch(`${COGNITO_DOMAIN}/oauth2/token`, {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
75
|
+
body: new URLSearchParams({
|
|
76
|
+
grant_type: 'authorization_code',
|
|
77
|
+
client_id: CLI_CLIENT_ID,
|
|
78
|
+
code,
|
|
79
|
+
redirect_uri: CALLBACK_URL,
|
|
80
|
+
code_verifier: codeVerifier,
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const tokens = await tokenRes.json();
|
|
85
|
+
|
|
86
|
+
if (!tokenRes.ok || tokens.error) {
|
|
87
|
+
throw new Error(tokens.error_description || tokens.error || 'Token exchange failed');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Decode ID token payload (no verification needed client-side)
|
|
91
|
+
const payload = JSON.parse(
|
|
92
|
+
Buffer.from(tokens.id_token.split('.')[1], 'base64').toString()
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const creds = {
|
|
96
|
+
refreshToken: tokens.refresh_token,
|
|
97
|
+
idToken: tokens.id_token,
|
|
98
|
+
accessToken: tokens.access_token,
|
|
99
|
+
expiresAt: Date.now() + tokens.expires_in * 1000,
|
|
100
|
+
email: payload.email || payload['cognito:username'],
|
|
101
|
+
sub: payload.sub,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
saveCredentials(creds);
|
|
105
|
+
|
|
106
|
+
res.writeHead(302, { Location: 'https://app.trucontext.ai' });
|
|
107
|
+
res.end();
|
|
108
|
+
|
|
109
|
+
server.close();
|
|
110
|
+
resolve(creds);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
113
|
+
res.end(`<html><body><h2>Login failed</h2><p>${err.message}</p></body></html>`);
|
|
114
|
+
server.close();
|
|
115
|
+
reject(err);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
server.listen(CALLBACK_PORT, () => {
|
|
120
|
+
const authorizeUrl = new URL(`${COGNITO_DOMAIN}/oauth2/authorize`);
|
|
121
|
+
authorizeUrl.searchParams.set('response_type', 'code');
|
|
122
|
+
authorizeUrl.searchParams.set('client_id', CLI_CLIENT_ID);
|
|
123
|
+
authorizeUrl.searchParams.set('redirect_uri', CALLBACK_URL);
|
|
124
|
+
authorizeUrl.searchParams.set('scope', 'email openid profile');
|
|
125
|
+
authorizeUrl.searchParams.set('code_challenge', codeChallenge);
|
|
126
|
+
authorizeUrl.searchParams.set('code_challenge_method', 'S256');
|
|
127
|
+
authorizeUrl.searchParams.set('state', state);
|
|
128
|
+
|
|
129
|
+
// Skip hosted UI and go straight to provider if specified
|
|
130
|
+
if (provider) {
|
|
131
|
+
authorizeUrl.searchParams.set('identity_provider', provider);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
open(authorizeUrl.toString());
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Timeout after 2 minutes
|
|
138
|
+
setTimeout(() => {
|
|
139
|
+
server.close();
|
|
140
|
+
reject(new Error('Login timed out — no callback received within 2 minutes'));
|
|
141
|
+
}, 120000);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Refresh tokens using the stored refresh token.
|
|
147
|
+
* Returns the new ID token.
|
|
148
|
+
*/
|
|
149
|
+
export async function refreshTokens() {
|
|
150
|
+
const creds = getCredentials();
|
|
151
|
+
if (!creds?.refreshToken) {
|
|
152
|
+
throw new Error('Not logged in. Run: trucontext login');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const res = await fetch(`${COGNITO_DOMAIN}/oauth2/token`, {
|
|
156
|
+
method: 'POST',
|
|
157
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
158
|
+
body: new URLSearchParams({
|
|
159
|
+
grant_type: 'refresh_token',
|
|
160
|
+
client_id: CLI_CLIENT_ID,
|
|
161
|
+
refresh_token: creds.refreshToken,
|
|
162
|
+
}),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const data = await res.json();
|
|
166
|
+
|
|
167
|
+
if (!res.ok || data.error) {
|
|
168
|
+
throw new Error(data.error_description || data.error || 'Token refresh failed. Run: trucontext login');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
saveCredentials({
|
|
172
|
+
...creds,
|
|
173
|
+
idToken: data.id_token,
|
|
174
|
+
accessToken: data.access_token,
|
|
175
|
+
expiresAt: Date.now() + data.expires_in * 1000,
|
|
176
|
+
// Cognito does NOT return a new refresh_token on refresh — keep the original
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return data.id_token;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get a valid ID token, refreshing if needed.
|
|
184
|
+
*/
|
|
185
|
+
export async function ensureFreshToken() {
|
|
186
|
+
const creds = getCredentials();
|
|
187
|
+
if (!creds) {
|
|
188
|
+
throw new Error('Not logged in. Run: trucontext login');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Refresh if within 60 seconds of expiry
|
|
192
|
+
if (Date.now() > (creds.expiresAt || 0) - 60000) {
|
|
193
|
+
return await refreshTokens();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return creds.idToken;
|
|
197
|
+
}
|
package/src/client.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// client.js — HTTP client for both API planes
|
|
2
|
+
// Auto-refreshes JWT, attaches active app header.
|
|
3
|
+
|
|
4
|
+
import { ensureFreshToken } from './auth.js';
|
|
5
|
+
import { getActiveApp, DATA_PLANE_URL, CONTROL_PLANE_URL } from './config.js';
|
|
6
|
+
|
|
7
|
+
async function request(baseUrl, method, path, body, retry = true) {
|
|
8
|
+
const token = await ensureFreshToken();
|
|
9
|
+
const activeApp = getActiveApp();
|
|
10
|
+
|
|
11
|
+
const headers = {
|
|
12
|
+
'Authorization': `Bearer ${token}`,
|
|
13
|
+
'Content-Type': 'application/json',
|
|
14
|
+
};
|
|
15
|
+
if (activeApp) {
|
|
16
|
+
headers['X-TruContext-App'] = activeApp;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const ac = new AbortController();
|
|
20
|
+
const timeout = setTimeout(() => ac.abort(), 15000);
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
24
|
+
method,
|
|
25
|
+
headers,
|
|
26
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
27
|
+
signal: ac.signal,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Retry once on 401 after refresh
|
|
31
|
+
if (res.status === 401 && retry) {
|
|
32
|
+
const newToken = await ensureFreshToken();
|
|
33
|
+
headers['Authorization'] = `Bearer ${newToken}`;
|
|
34
|
+
const retryRes = await fetch(`${baseUrl}${path}`, {
|
|
35
|
+
method,
|
|
36
|
+
headers,
|
|
37
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
38
|
+
signal: ac.signal,
|
|
39
|
+
});
|
|
40
|
+
return handleResponse(retryRes);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return handleResponse(res);
|
|
44
|
+
} finally {
|
|
45
|
+
clearTimeout(timeout);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function handleResponse(res) {
|
|
50
|
+
const text = await res.text();
|
|
51
|
+
let data;
|
|
52
|
+
try {
|
|
53
|
+
data = JSON.parse(text);
|
|
54
|
+
} catch {
|
|
55
|
+
data = { raw: text };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!res.ok) {
|
|
59
|
+
const msg = data?.error || data?.message || `HTTP ${res.status}`;
|
|
60
|
+
const err = new Error(msg);
|
|
61
|
+
err.status = res.status;
|
|
62
|
+
err.data = data;
|
|
63
|
+
throw err;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return data;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// --- Public API ---
|
|
70
|
+
|
|
71
|
+
export function dataPlane(method, path, body) {
|
|
72
|
+
return request(DATA_PLANE_URL, method, path, body);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function controlPlane(method, path, body) {
|
|
76
|
+
return request(CONTROL_PLANE_URL, method, path, body);
|
|
77
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { controlPlane } from '../client.js';
|
|
3
|
+
import { getActiveApp, setActiveApp } from '../config.js';
|
|
4
|
+
|
|
5
|
+
export async function appsCommand() {
|
|
6
|
+
try {
|
|
7
|
+
const res = await controlPlane('GET', '/apps');
|
|
8
|
+
const apps = res.data?.apps || [];
|
|
9
|
+
const activeApp = getActiveApp();
|
|
10
|
+
|
|
11
|
+
if (apps.length === 0) {
|
|
12
|
+
console.log(chalk.yellow('No apps. Create one in the dashboard.'));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
for (const app of apps) {
|
|
17
|
+
const active = app.appId === activeApp ? chalk.green(' (active)') : '';
|
|
18
|
+
console.log(`${chalk.bold(app.name)}${active}`);
|
|
19
|
+
console.log(` ${chalk.dim(app.appId)} — ${app.status}`);
|
|
20
|
+
}
|
|
21
|
+
} catch (err) {
|
|
22
|
+
console.error(chalk.red(`Failed: ${err.message}`));
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function useCommand(appId) {
|
|
28
|
+
try {
|
|
29
|
+
const res = await controlPlane('GET', '/apps');
|
|
30
|
+
const apps = res.data?.apps || [];
|
|
31
|
+
const app = apps.find(a => a.appId === appId || a.name.toLowerCase() === appId.toLowerCase());
|
|
32
|
+
|
|
33
|
+
if (!app) {
|
|
34
|
+
console.error(chalk.red(`App not found: ${appId}`));
|
|
35
|
+
console.log(chalk.dim('Available apps:'));
|
|
36
|
+
for (const a of apps) {
|
|
37
|
+
console.log(` ${a.name} ${chalk.dim(a.appId)}`);
|
|
38
|
+
}
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
setActiveApp(app.appId);
|
|
43
|
+
console.log(chalk.green(`Active app: ${chalk.bold(app.name)} (${app.appId})`));
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.error(chalk.red(`Failed: ${err.message}`));
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { dataPlane } from '../client.js';
|
|
3
|
+
|
|
4
|
+
export async function contextsListCommand() {
|
|
5
|
+
try {
|
|
6
|
+
const res = await dataPlane('GET', '/v1/contexts');
|
|
7
|
+
const contexts = res.data?.contexts || [];
|
|
8
|
+
|
|
9
|
+
if (contexts.length === 0) {
|
|
10
|
+
console.log(chalk.yellow('No contexts. Create one with: trucontext contexts create <name>'));
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
for (const ctx of contexts) {
|
|
15
|
+
console.log(`${chalk.bold(ctx.name)} ${chalk.dim(ctx.contextId)}`);
|
|
16
|
+
if (ctx.metadata && Object.keys(ctx.metadata).length > 0) {
|
|
17
|
+
console.log(` ${chalk.dim(JSON.stringify(ctx.metadata))}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.error(chalk.red(`Failed: ${err.message}`));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function contextsCreateCommand(name, options) {
|
|
27
|
+
try {
|
|
28
|
+
const body = { name };
|
|
29
|
+
if (options.metadata) {
|
|
30
|
+
try {
|
|
31
|
+
body.metadata = JSON.parse(options.metadata);
|
|
32
|
+
} catch {
|
|
33
|
+
console.error(chalk.red('Invalid metadata JSON'));
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const res = await dataPlane('POST', '/v1/contexts', body);
|
|
39
|
+
const ctx = res.data;
|
|
40
|
+
console.log(chalk.green(`Created: ${chalk.bold(ctx.name)}`));
|
|
41
|
+
console.log(chalk.dim(`Context ID: ${ctx.contextId}`));
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.error(chalk.red(`Failed: ${err.message}`));
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function contextsDeleteCommand(contextId) {
|
|
49
|
+
try {
|
|
50
|
+
await dataPlane('DELETE', `/v1/contexts/${contextId}`);
|
|
51
|
+
console.log(chalk.green(`Deleted: ${contextId}`));
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.error(chalk.red(`Failed: ${err.message}`));
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { dataPlane } from '../client.js';
|
|
3
|
+
|
|
4
|
+
export async function entitiesListCommand(options) {
|
|
5
|
+
try {
|
|
6
|
+
const params = new URLSearchParams();
|
|
7
|
+
if (options.type) params.set('type', options.type);
|
|
8
|
+
if (options.limit) params.set('limit', options.limit);
|
|
9
|
+
const qs = params.toString();
|
|
10
|
+
const path = `/v1/entities${qs ? `?${qs}` : ''}`;
|
|
11
|
+
|
|
12
|
+
const res = await dataPlane('GET', path);
|
|
13
|
+
const entities = res.data?.entities || [];
|
|
14
|
+
|
|
15
|
+
if (entities.length === 0) {
|
|
16
|
+
console.log(chalk.yellow('No entities found.'));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
for (const e of entities) {
|
|
21
|
+
console.log(`${chalk.bold(e.entityId)} ${chalk.dim(`[${e.type}]`)}`);
|
|
22
|
+
if (e.recipe_id) console.log(` recipe: ${chalk.dim(e.recipe_id)}`);
|
|
23
|
+
if (e.properties && Object.keys(e.properties).length > 0) {
|
|
24
|
+
console.log(` properties: ${chalk.dim(JSON.stringify(e.properties))}`);
|
|
25
|
+
}
|
|
26
|
+
if (e.heartbeat_enabled !== undefined) console.log(` heartbeat: ${chalk.dim(e.heartbeat_enabled)}`);
|
|
27
|
+
if (e.confidence !== undefined) console.log(` confidence: ${chalk.dim(e.confidence)}`);
|
|
28
|
+
if (e.temporal !== undefined) console.log(` temporal: ${chalk.dim(e.temporal)}`);
|
|
29
|
+
}
|
|
30
|
+
} catch (err) {
|
|
31
|
+
console.error(chalk.red(`Failed: ${err.message}`));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function entitiesGetCommand(entityId) {
|
|
37
|
+
try {
|
|
38
|
+
const res = await dataPlane('GET', `/v1/entities/${entityId}`);
|
|
39
|
+
const e = res.data || res;
|
|
40
|
+
|
|
41
|
+
console.log(`${chalk.bold(e.entityId)} ${chalk.dim(`[${e.type}]`)}`);
|
|
42
|
+
if (e.recipe_id) console.log(` recipe: ${chalk.dim(e.recipe_id)}`);
|
|
43
|
+
if (e.properties && Object.keys(e.properties).length > 0) {
|
|
44
|
+
console.log(` properties: ${chalk.dim(JSON.stringify(e.properties))}`);
|
|
45
|
+
}
|
|
46
|
+
if (e.heartbeat_enabled !== undefined) console.log(` heartbeat: ${chalk.dim(e.heartbeat_enabled)}`);
|
|
47
|
+
if (e.confidence !== undefined) console.log(` confidence: ${chalk.dim(e.confidence)}`);
|
|
48
|
+
if (e.temporal !== undefined) console.log(` temporal: ${chalk.dim(e.temporal)}`);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.error(chalk.red(`Failed: ${err.message}`));
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function entitiesCreateCommand(options) {
|
|
56
|
+
try {
|
|
57
|
+
if (!options.id) {
|
|
58
|
+
console.error(chalk.red('--id is required'));
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
if (!options.type) {
|
|
62
|
+
console.error(chalk.red('--type is required'));
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const body = {
|
|
67
|
+
entityId: options.id,
|
|
68
|
+
type: options.type,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (options.properties) {
|
|
72
|
+
try {
|
|
73
|
+
body.properties = JSON.parse(options.properties);
|
|
74
|
+
} catch {
|
|
75
|
+
console.error(chalk.red('Invalid properties JSON'));
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (options.recipe) body.recipe_id = options.recipe;
|
|
81
|
+
if (options.heartbeat === false) body.heartbeat_enabled = false;
|
|
82
|
+
if (options.confidence !== undefined) body.confidence = parseFloat(options.confidence);
|
|
83
|
+
if (options.temporal !== undefined) body.temporal = options.temporal;
|
|
84
|
+
|
|
85
|
+
if (options.context?.length > 0) {
|
|
86
|
+
body.contexts = options.context.map(entry => {
|
|
87
|
+
const colonIdx = entry.indexOf(':');
|
|
88
|
+
if (colonIdx === -1) {
|
|
89
|
+
return { context_id: entry, relationship: 'ABOUT' };
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
context_id: entry.slice(0, colonIdx),
|
|
93
|
+
relationship: entry.slice(colonIdx + 1),
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const res = await dataPlane('POST', '/v1/entities', body);
|
|
99
|
+
const e = res.data || res;
|
|
100
|
+
console.log(chalk.green(`Created: ${chalk.bold(e.entityId || options.id)}`));
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.error(chalk.red(`Failed: ${err.message}`));
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function entitiesUpdateCommand(entityId, options) {
|
|
108
|
+
try {
|
|
109
|
+
const body = {};
|
|
110
|
+
|
|
111
|
+
if (options.properties) {
|
|
112
|
+
try {
|
|
113
|
+
body.properties = JSON.parse(options.properties);
|
|
114
|
+
} catch {
|
|
115
|
+
console.error(chalk.red('Invalid properties JSON'));
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (options.recipe) body.recipe_id = options.recipe;
|
|
121
|
+
if (options.heartbeat !== undefined) body.heartbeat_enabled = options.heartbeat;
|
|
122
|
+
if (options.confidence !== undefined) body.confidence = parseFloat(options.confidence);
|
|
123
|
+
if (options.temporal !== undefined) body.temporal = options.temporal;
|
|
124
|
+
|
|
125
|
+
await dataPlane('PUT', `/v1/entities/${entityId}`, body);
|
|
126
|
+
console.log(chalk.green(`Updated: ${entityId}`));
|
|
127
|
+
} catch (err) {
|
|
128
|
+
console.error(chalk.red(`Failed: ${err.message}`));
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function entitiesDeleteCommand(entityId) {
|
|
134
|
+
try {
|
|
135
|
+
await dataPlane('DELETE', `/v1/entities/${entityId}`);
|
|
136
|
+
console.log(chalk.green(`Deleted: ${entityId}`));
|
|
137
|
+
} catch (err) {
|
|
138
|
+
console.error(chalk.red(`Failed: ${err.message}`));
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { readFileSync, existsSync } from 'fs';
|
|
3
|
+
import { basename } from 'path';
|
|
4
|
+
import { dataPlane } from '../client.js';
|
|
5
|
+
|
|
6
|
+
export async function ingestCommand(source, options) {
|
|
7
|
+
try {
|
|
8
|
+
let body;
|
|
9
|
+
|
|
10
|
+
if (existsSync(source)) {
|
|
11
|
+
const content = readFileSync(source);
|
|
12
|
+
const filename = basename(source);
|
|
13
|
+
const ext = filename.split('.').pop().toLowerCase();
|
|
14
|
+
|
|
15
|
+
const textExts = ['txt', 'md', 'csv', 'html', 'json'];
|
|
16
|
+
const isText = textExts.includes(ext);
|
|
17
|
+
|
|
18
|
+
body = {
|
|
19
|
+
content: isText ? content.toString('utf-8') : content.toString('base64'),
|
|
20
|
+
content_type: isText ? 'text/plain' : `application/${ext}`,
|
|
21
|
+
encoding: isText ? undefined : 'base64',
|
|
22
|
+
metadata: { original_filename: filename, source_medium: 'cli' },
|
|
23
|
+
};
|
|
24
|
+
} else {
|
|
25
|
+
body = {
|
|
26
|
+
content: source,
|
|
27
|
+
content_type: 'text/plain',
|
|
28
|
+
metadata: { source_medium: 'cli' },
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (options.confidence !== undefined) body.confidence = parseFloat(options.confidence);
|
|
33
|
+
if (options.temporal !== undefined) body.temporal = options.temporal;
|
|
34
|
+
|
|
35
|
+
// Build contexts array from --context flags: "entity-id:RELATIONSHIP"
|
|
36
|
+
if (options.context?.length > 0) {
|
|
37
|
+
body.contexts = options.context.map(entry => {
|
|
38
|
+
const colonIdx = entry.indexOf(':');
|
|
39
|
+
if (colonIdx === -1) {
|
|
40
|
+
return { context_id: entry, relationship: 'ABOUT' };
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
context_id: entry.slice(0, colonIdx),
|
|
44
|
+
relationship: entry.slice(colonIdx + 1),
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const res = await dataPlane('POST', '/v1/ingest', body);
|
|
50
|
+
console.log(chalk.green('Accepted'));
|
|
51
|
+
console.log(chalk.dim(`Content ID: ${res.contentId}`));
|
|
52
|
+
if (res.status === 'upload_required') {
|
|
53
|
+
console.log(chalk.yellow(`Large file — upload to: ${res.uploadUrl}`));
|
|
54
|
+
}
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error(chalk.red(`Ingest failed: ${err.message}`));
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import input from '@inquirer/input';
|
|
3
|
+
import select from '@inquirer/select';
|
|
4
|
+
import { controlPlane } from '../client.js';
|
|
5
|
+
import { setActiveApp } from '../config.js';
|
|
6
|
+
|
|
7
|
+
export async function initCommand(name, options) {
|
|
8
|
+
// Step 1: App name
|
|
9
|
+
if (!name) {
|
|
10
|
+
name = await input({ message: 'App name:' });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Step 2: Description (required for schema generation)
|
|
14
|
+
let description = options.description;
|
|
15
|
+
if (!description) {
|
|
16
|
+
description = await input({
|
|
17
|
+
message: 'Describe your app (what data will it store, who uses it):',
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Step 3: Authorship model
|
|
22
|
+
const authorship = await select({
|
|
23
|
+
message: 'Who creates content in your app?',
|
|
24
|
+
choices: [
|
|
25
|
+
{ name: 'Multiple users contribute content', value: 'multi', description: 'e.g. team wikis, social apps, collaboration tools' },
|
|
26
|
+
{ name: 'Single author / personal use', value: 'single', description: 'e.g. personal notes, solo projects' },
|
|
27
|
+
{ name: 'The app itself generates content', value: 'app', description: 'e.g. automated pipelines, bots, scrapers' },
|
|
28
|
+
],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Step 4: Create the app
|
|
32
|
+
console.log(chalk.dim(`\nCreating app "${name}"...`));
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const res = await controlPlane('POST', '/apps', { name, description });
|
|
36
|
+
const app = res.data;
|
|
37
|
+
setActiveApp(app.appId);
|
|
38
|
+
|
|
39
|
+
console.log(chalk.green(`App created: ${app.appId}`));
|
|
40
|
+
|
|
41
|
+
// Step 5: Generate schema
|
|
42
|
+
console.log(chalk.dim('Generating schema from description...'));
|
|
43
|
+
|
|
44
|
+
const schemaRes = await controlPlane('POST', '/schema/generate', {
|
|
45
|
+
description,
|
|
46
|
+
authorship_model: authorship,
|
|
47
|
+
});
|
|
48
|
+
const schema = schemaRes.data?.schema;
|
|
49
|
+
|
|
50
|
+
if (schema) {
|
|
51
|
+
// Step 6: Save schema to app
|
|
52
|
+
await controlPlane('PUT', `/apps/${app.appId}/schema`, schema);
|
|
53
|
+
|
|
54
|
+
console.log(chalk.green('Schema generated and saved.\n'));
|
|
55
|
+
|
|
56
|
+
// Show a summary
|
|
57
|
+
if (schema.entity_types) {
|
|
58
|
+
console.log(chalk.bold('Entity types:'));
|
|
59
|
+
for (const et of schema.entity_types) {
|
|
60
|
+
console.log(` ${chalk.cyan(et.name)} ${chalk.dim(et.description || '')}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (schema.edge_types) {
|
|
64
|
+
console.log(chalk.bold('\nRelationships:'));
|
|
65
|
+
for (const edge of schema.edge_types) {
|
|
66
|
+
console.log(` ${chalk.dim(edge.from)} ${chalk.yellow(`—${edge.name}→`)} ${chalk.dim(edge.to)}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
console.log(chalk.yellow('Schema generation returned empty — you can generate one later:'));
|
|
71
|
+
console.log(chalk.cyan(' trucontext schema generate -d "your description"'));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log(chalk.dim(`\nReady to go:`));
|
|
75
|
+
console.log(chalk.cyan(' trucontext ingest <file>'));
|
|
76
|
+
console.log(chalk.cyan(' trucontext query "your question"'));
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.error(chalk.red(`Failed: ${err.message}`));
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
}
|