trucontext 0.5.0 → 0.6.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/bin/cli.js CHANGED
@@ -18,6 +18,7 @@ import { entitiesListCommand, entitiesGetCommand, entitiesCreateCommand, entitie
18
18
  import { recipesListCommand, recipesGetCommand, recipesCreateCommand, recipesDeleteCommand } from '../src/commands/recipes.js';
19
19
  import { relationshipTypesListCommand } from '../src/commands/relationship-types.js';
20
20
  import { docsListCommand, docsShowCommand } from '../src/commands/docs.js';
21
+ import { registerRootsCommand } from '../src/commands/roots.js';
21
22
 
22
23
  program
23
24
  .name('trucontext')
@@ -65,7 +66,8 @@ program.command('ingest <source>')
65
66
  // Query
66
67
  program.command('query <question>')
67
68
  .description('Query the knowledge graph')
68
- .option('-c, --context <id>', 'Scope to a context')
69
+ .option('-r, --root <id>', 'Root node ID (or use: trucontext roots use <id>)')
70
+ .option('-c, --context <id>', 'Further scope within the root ego network')
69
71
  .option('-m, --mode <mode>', 'Query mode: answer (default) or context', 'answer')
70
72
  .option('-l, --limit <n>', 'Max results', '20')
71
73
  .action(queryCommand);
@@ -73,7 +75,8 @@ program.command('query <question>')
73
75
  // Recall
74
76
  program.command('recall <query>')
75
77
  .description('Semantic memory recall')
76
- .option('-c, --context <id>', 'Scope to a context')
78
+ .option('-r, --root <id>', 'Root node ID (or use: trucontext roots use <id>)')
79
+ .option('-c, --context <id>', 'Further scope within the root ego network')
77
80
  .option('-l, --limit <n>', 'Max seed results', '10')
78
81
  .option('-d, --depth <n>', 'Graph expansion depth', '2')
79
82
  .action(recallCommand);
@@ -130,6 +133,9 @@ entities.command('edges <entityId>')
130
133
  .description('List edges for an entity')
131
134
  .action(entitiesEdgesCommand);
132
135
 
136
+ // Root Nodes
137
+ registerRootsCommand(program);
138
+
133
139
  // Recipes
134
140
  const recipes = program.command('recipes').description('Manage recipes').action(function() { this.help(); });
135
141
  recipes.command('list')
@@ -139,10 +145,11 @@ recipes.command('get <recipeId>')
139
145
  .description('Get recipe details')
140
146
  .action(recipesGetCommand);
141
147
  recipes.command('create')
142
- .description('Create a recipe')
143
- .requiredOption('--id <recipeId>', 'Recipe ID')
144
- .requiredOption('--name <name>', 'Recipe name')
145
- .requiredOption('--purpose <text>', 'Recipe purpose')
148
+ .description('Create a custom recipe. --id and --name are always required. Provide the interpretation via --file (JSON) or inline flags.')
149
+ .requiredOption('--id <recipeId>', 'Recipe ID (e.g., my-health-score)')
150
+ .requiredOption('--name <name>', 'Recipe display name')
151
+ .option('--file <path>', 'JSON file containing the interpretation block (the WHY)')
152
+ .option('--purpose <text>', 'Recipe purpose (inline alternative to --file)')
146
153
  .option('--decay-profile <text>', 'Decay profile description')
147
154
  .option('--confidence-bias <text>', 'Confidence bias description')
148
155
  .action(recipesCreateCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trucontext",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "TruContext CLI — contextual memory for AI applications",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,20 +32,24 @@ export async function ingestCommand(source, options) {
32
32
  if (options.confidence !== undefined) body.confidence = parseFloat(options.confidence);
33
33
  if (options.temporal !== undefined) body.temporal = options.temporal;
34
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
- });
35
+ // Build contexts array from --context flags: "entity-id:RELATIONSHIP" (required)
36
+ if (!options.context || options.context.length === 0) {
37
+ console.error(chalk.red('At least one context is required.'));
38
+ console.error(chalk.dim('Use: --context entity-id:RELATIONSHIP (e.g., --context dustin:ABOUT)'));
39
+ process.exit(1);
47
40
  }
48
41
 
42
+ body.contexts = options.context.map(entry => {
43
+ const colonIdx = entry.indexOf(':');
44
+ if (colonIdx === -1) {
45
+ return { context_id: entry, relationship: 'ABOUT' };
46
+ }
47
+ return {
48
+ context_id: entry.slice(0, colonIdx),
49
+ relationship: entry.slice(colonIdx + 1),
50
+ };
51
+ });
52
+
49
53
  const res = await dataPlane('POST', '/v1/ingest', body);
50
54
  console.log(chalk.green('Accepted'));
51
55
  console.log(chalk.dim(`Content ID: ${res.contentId}`));
@@ -71,8 +71,62 @@ export async function initCommand(name, options) {
71
71
  console.log(chalk.cyan(' trucontext schema generate -d "your description"'));
72
72
  }
73
73
 
74
- console.log(chalk.dim(`\nReady to go:`));
75
- console.log(chalk.cyan(' trucontext ingest <file>'));
74
+ // Step 7: Create first root node
75
+ console.log(chalk.bold('\nCreate Your First Root Node'));
76
+ console.log(chalk.dim('Root nodes are the intelligence boundaries of your graph.\n'));
77
+
78
+ const rootId = await input({ message: 'Root node ID (e.g., my-agent, dustin):' });
79
+ const rootType = await select({
80
+ message: 'Root node type:',
81
+ choices: [
82
+ { name: 'Person', value: 'Person' },
83
+ { name: 'Agent', value: 'Agent' },
84
+ { name: 'Organization', value: 'Organization' },
85
+ { name: 'Business', value: 'Business' },
86
+ { name: 'Product', value: 'Product' },
87
+ { name: 'Project', value: 'Project' },
88
+ ],
89
+ });
90
+
91
+ const rootRecipe = await select({
92
+ message: 'Recipe (how should the system think about this node\'s data?):',
93
+ choices: [
94
+ { name: 'Personal Assistant Memory', value: 'recipe:personal-assistant-memory' },
95
+ { name: 'Meeting Memory', value: 'recipe:meeting-memory' },
96
+ { name: 'Customer Voice', value: 'recipe:customer-voice' },
97
+ { name: 'Competitive Intelligence', value: 'recipe:competitive-intelligence' },
98
+ { name: 'AI Agent Self-Reflection', value: 'recipe:ai-agent-self-reflection' },
99
+ { name: 'Organizational Context', value: 'recipe:organizational-context' },
100
+ { name: 'Decision Archive', value: 'recipe:decision-archive' },
101
+ { name: 'Curiosity Loop', value: 'recipe:curiosity-loop' },
102
+ ],
103
+ });
104
+
105
+ const rootName = await input({ message: 'Display name (e.g., Dustin Henderson):' });
106
+
107
+ try {
108
+ await controlPlane('POST', '/roots', {
109
+ entityId: rootId,
110
+ type: rootType,
111
+ recipe_id: rootRecipe,
112
+ dreamers: 'all',
113
+ properties: rootName ? { name: rootName } : {},
114
+ });
115
+
116
+ const { saveConfig, getConfig } = await import('../config.js');
117
+ const config = getConfig();
118
+ config.activeRoot = rootId;
119
+ saveConfig(config);
120
+
121
+ console.log(chalk.green(`Root node created: ${rootId}`));
122
+ console.log(chalk.green(`Active root set: ${rootId}\n`));
123
+ } catch (rootErr) {
124
+ console.error(chalk.yellow(`Root creation failed: ${rootErr.response?.data?.error || rootErr.message}`));
125
+ console.log(chalk.dim('You can create one later: trucontext roots create --id <id> --type <type> --recipe <recipe>\n'));
126
+ }
127
+
128
+ console.log(chalk.dim('Ready to go:'));
129
+ console.log(chalk.cyan(' trucontext ingest "content" --context ' + rootId + ':ABOUT'));
76
130
  console.log(chalk.cyan(' trucontext query "your question"'));
77
131
  } catch (err) {
78
132
  console.error(chalk.red(`Failed: ${err.message}`));
@@ -1,10 +1,19 @@
1
1
  import chalk from 'chalk';
2
2
  import { dataPlane } from '../client.js';
3
+ import { getConfig } from '../config.js';
3
4
 
4
5
  export async function queryCommand(question, options) {
5
6
  try {
7
+ // root_id is required — from --root flag or active root
8
+ const rootId = options.root || getConfig().activeRoot;
9
+ if (!rootId) {
10
+ console.error(chalk.red('Root node required. Use --root <id> or set active root: trucontext roots use <id>'));
11
+ process.exit(1);
12
+ }
13
+
6
14
  const body = {
7
15
  mode: options.mode === 'context' ? 'CONTEXT' : 'ANSWER',
16
+ root_id: rootId,
8
17
  max_results: options.limit ? parseInt(options.limit, 10) : 20,
9
18
  };
10
19
 
@@ -1,10 +1,19 @@
1
1
  import chalk from 'chalk';
2
2
  import { dataPlane } from '../client.js';
3
+ import { getConfig } from '../config.js';
3
4
 
4
5
  export async function recallCommand(query, options) {
5
6
  try {
7
+ // root_id is required — from --root flag or active root
8
+ const rootId = options.root || getConfig().activeRoot;
9
+ if (!rootId) {
10
+ console.error(chalk.red('Root node required. Use --root <id> or set active root: trucontext roots use <id>'));
11
+ process.exit(1);
12
+ }
13
+
6
14
  const body = {
7
15
  query,
16
+ root_id: rootId,
8
17
  maxResults: options.limit ? parseInt(options.limit, 10) : 10,
9
18
  expansionDepth: options.depth ? parseInt(options.depth, 10) : 2,
10
19
  };
@@ -1,3 +1,4 @@
1
+ import { readFileSync } from 'fs';
1
2
  import chalk from 'chalk';
2
3
  import { controlPlane } from '../client.js';
3
4
  import { getActiveApp } from '../config.js';
@@ -107,19 +108,35 @@ export async function recipesCreateCommand(options) {
107
108
  console.error(chalk.red('--name is required'));
108
109
  process.exit(1);
109
110
  }
110
- if (!options.purpose) {
111
- console.error(chalk.red('--purpose is required'));
112
- process.exit(1);
113
- }
114
111
 
115
112
  const body = {
116
113
  recipeId: options.id,
117
114
  name: options.name,
118
- purpose: options.purpose,
119
115
  };
120
116
 
121
- if (options.decayProfile) body.decay_profile = options.decayProfile;
122
- if (options.confidenceBias) body.confidence_bias = options.confidenceBias;
117
+ // --file provides the full interpretation block as JSON
118
+ if (options.file) {
119
+ try {
120
+ const raw = readFileSync(options.file, 'utf8');
121
+ body.interpretation = JSON.parse(raw);
122
+ if (!body.interpretation.purpose) {
123
+ console.error(chalk.red('File must contain a "purpose" field'));
124
+ process.exit(1);
125
+ }
126
+ } catch (e) {
127
+ console.error(chalk.red(`Failed to read --file: ${e.message}`));
128
+ process.exit(1);
129
+ }
130
+ } else {
131
+ // Inline flags → build interpretation object
132
+ if (!options.purpose) {
133
+ console.error(chalk.red('--purpose is required (or use --file to provide the full interpretation)'));
134
+ process.exit(1);
135
+ }
136
+ body.interpretation = { purpose: options.purpose };
137
+ if (options.decayProfile) body.interpretation.decay_profile = options.decayProfile;
138
+ if (options.confidenceBias) body.interpretation.confidence_bias = options.confidenceBias;
139
+ }
123
140
 
124
141
  const res = await controlPlane('POST', `/apps/${appId}/recipes`, body);
125
142
  const r = res.data || res;
@@ -0,0 +1,195 @@
1
+ // roots.js — Root Node commands
2
+ //
3
+ // Root nodes are the intelligence boundaries of the graph. They define ego networks,
4
+ // carry recipes, and control which dreams run. Queries require a root_id.
5
+
6
+ import chalk from 'chalk';
7
+ import { controlPlane } from '../client.js';
8
+ import { getConfig, saveConfig } from '../config.js';
9
+
10
+ export function registerRootsCommand(program) {
11
+ const roots = program
12
+ .command('roots')
13
+ .description('Manage root nodes — intelligence boundaries of the graph')
14
+ .action(function() { this.help(); });
15
+
16
+ // CREATE
17
+ roots
18
+ .command('create')
19
+ .description('Create a root node')
20
+ .requiredOption('--id <entityId>', 'Unique identifier for the root node')
21
+ .requiredOption('--type <type>', 'Node type (e.g., Person, Agent, Organization)')
22
+ .requiredOption('--recipe <recipeId>', 'Recipe ID (e.g., recipe:personal-assistant-memory)')
23
+ .option('--dreamers <list>', 'Dreamers: "all" or comma-separated (e.g., decay,curiosity)', 'all')
24
+ .option('--hop-depth <n>', 'Max traversal depth for ego network (default: unlimited)', parseInt)
25
+ .option('--do-not-follow', 'Mark as structural root — do not traverse into from other roots')
26
+ .option('--properties <json>', 'JSON properties object')
27
+ .option('-c, --context <entry>', 'Link to other roots: entity-id:RELATIONSHIP (repeatable)', (val, prev) => [...prev, val], [])
28
+ .action(async (opts) => {
29
+ try {
30
+ const body = {
31
+ entityId: opts.id,
32
+ type: opts.type,
33
+ recipe_id: opts.recipe,
34
+ dreamers: opts.dreamers === 'all' ? 'all' : opts.dreamers.split(',').map(s => s.trim()),
35
+ hop_depth: opts.hopDepth || null,
36
+ do_not_follow: opts.doNotFollow || false,
37
+ };
38
+
39
+ if (opts.properties) {
40
+ try { body.properties = JSON.parse(opts.properties); } catch {
41
+ console.error(chalk.red('Error: --properties must be valid JSON'));
42
+ process.exit(1);
43
+ }
44
+ }
45
+
46
+ if (opts.context && opts.context.length > 0) {
47
+ body.contexts = opts.context.map(c => {
48
+ const [id, rel] = c.split(':');
49
+ if (!id || !rel) {
50
+ console.error(chalk.red(`Error: Invalid context format "${c}". Use entity-id:RELATIONSHIP`));
51
+ process.exit(1);
52
+ }
53
+ return { context_id: id, relationship: rel };
54
+ });
55
+ }
56
+
57
+ const res = await controlPlane('POST', '/roots', body);
58
+ const r = res.data;
59
+ console.log(chalk.green(`Root node created: ${r.entityId}`));
60
+ console.log(` Type: ${r.type}`);
61
+ console.log(` Recipe: ${r.recipe_id}`);
62
+ console.log(` Dreamers: ${Array.isArray(r.dreamers) ? r.dreamers.join(', ') : r.dreamers}`);
63
+ console.log(` Hop depth: ${r.hop_depth || 'unlimited'}`);
64
+ } catch (err) {
65
+ console.error(chalk.red('Failed:'), err.response?.data?.error || err.response?.data?.details || err.message);
66
+ process.exit(1);
67
+ }
68
+ });
69
+
70
+ // LIST
71
+ roots
72
+ .command('list')
73
+ .description('List all root nodes for the active app')
74
+ .action(async () => {
75
+ try {
76
+ const res = await controlPlane('GET', '/roots');
77
+ const rootNodes = res.data?.roots || [];
78
+ if (rootNodes.length === 0) {
79
+ console.log('No root nodes. Create one with: trucontext roots create');
80
+ return;
81
+ }
82
+
83
+ const config = getConfig();
84
+ for (const r of rootNodes) {
85
+ const active = config.activeRoot === r.entityId ? chalk.cyan(' (active)') : '';
86
+ const dreamers = Array.isArray(r.dreamers) ? r.dreamers.join(', ') : r.dreamers;
87
+ console.log(`${chalk.bold(r.properties?.name || r.entityId)}${active}`);
88
+ console.log(` ${r.entityId} — ${r.type} — recipe: ${r.recipe_id || 'none'} — dreamers: ${dreamers}`);
89
+ }
90
+ } catch (err) {
91
+ console.error(chalk.red('Failed:'), err.response?.data?.error || err.message);
92
+ process.exit(1);
93
+ }
94
+ });
95
+
96
+ // SHOW
97
+ roots
98
+ .command('show <rootId>')
99
+ .description('Show details of a root node')
100
+ .action(async (rootId) => {
101
+ try {
102
+ const res = await controlPlane('GET', `/roots/${rootId}`);
103
+ const r = res.data;
104
+ console.log(chalk.bold(`Root: ${r.entityId}`));
105
+ console.log(` Type: ${r.type}`);
106
+ console.log(` Recipe: ${r.recipe_id || 'none'}`);
107
+ console.log(` Dreamers: ${Array.isArray(r.dreamers) ? r.dreamers.join(', ') : r.dreamers}`);
108
+ console.log(` Hop depth: ${r.hop_depth || 'unlimited'}`);
109
+ console.log(` Do not follow: ${r.do_not_follow}`);
110
+
111
+ if (r.linkedRoots && r.linkedRoots.length > 0) {
112
+ console.log(` Linked roots:`);
113
+ for (const lr of r.linkedRoots) {
114
+ console.log(` → ${lr.entityId} (${lr.type}) via ${lr.relationship}`);
115
+ }
116
+ }
117
+ } catch (err) {
118
+ console.error(chalk.red('Failed:'), err.response?.data?.error || err.message);
119
+ process.exit(1);
120
+ }
121
+ });
122
+
123
+ // UPDATE
124
+ roots
125
+ .command('update <rootId>')
126
+ .description('Update a root node')
127
+ .option('--recipe <recipeId>', 'New recipe ID')
128
+ .option('--dreamers <list>', 'New dreamers: "all" or comma-separated')
129
+ .option('--hop-depth <n>', 'New hop depth', parseInt)
130
+ .option('--do-not-follow', 'Set do-not-follow flag')
131
+ .option('--no-do-not-follow', 'Clear do-not-follow flag')
132
+ .option('--properties <json>', 'JSON properties to update')
133
+ .action(async (rootId, opts) => {
134
+ try {
135
+ const body = {};
136
+ if (opts.recipe) body.recipe_id = opts.recipe;
137
+ if (opts.dreamers) body.dreamers = opts.dreamers === 'all' ? 'all' : opts.dreamers.split(',').map(s => s.trim());
138
+ if (opts.hopDepth !== undefined) body.hop_depth = opts.hopDepth;
139
+ if (opts.doNotFollow !== undefined) body.do_not_follow = opts.doNotFollow;
140
+ if (opts.properties) {
141
+ try { body.properties = JSON.parse(opts.properties); } catch {
142
+ console.error(chalk.red('Error: --properties must be valid JSON'));
143
+ process.exit(1);
144
+ }
145
+ }
146
+
147
+ await controlPlane('PUT', `/roots/${rootId}`, body);
148
+ console.log(chalk.green(`Updated: ${rootId}`));
149
+ } catch (err) {
150
+ console.error(chalk.red('Failed:'), err.response?.data?.error || err.message);
151
+ process.exit(1);
152
+ }
153
+ });
154
+
155
+ // DELETE (demote)
156
+ roots
157
+ .command('delete <rootId>')
158
+ .description('Demote a root node to a regular entity (does not delete the node)')
159
+ .action(async (rootId) => {
160
+ try {
161
+ await controlPlane('DELETE', `/roots/${rootId}`);
162
+ console.log(chalk.green(`Demoted: ${rootId} (no longer a root node)`));
163
+
164
+ const config = getConfig();
165
+ if (config.activeRoot === rootId) {
166
+ config.activeRoot = undefined;
167
+ saveConfig(config);
168
+ }
169
+ } catch (err) {
170
+ console.error(chalk.red('Failed:'), err.response?.data?.error || err.message);
171
+ process.exit(1);
172
+ }
173
+ });
174
+
175
+ // USE — set active root for queries
176
+ roots
177
+ .command('use <rootId>')
178
+ .description('Set the active root node for queries and recall')
179
+ .action(async (rootId) => {
180
+ try {
181
+ await controlPlane('GET', `/roots/${rootId}`);
182
+ const config = getConfig();
183
+ config.activeRoot = rootId;
184
+ saveConfig(config);
185
+ console.log(chalk.green(`Active root: ${rootId}`));
186
+ } catch (err) {
187
+ if (err.response?.status === 404) {
188
+ console.error(chalk.red(`Root node '${rootId}' not found. Run: trucontext roots list`));
189
+ } else {
190
+ console.error(chalk.red('Failed:'), err.response?.data?.error || err.message);
191
+ }
192
+ process.exit(1);
193
+ }
194
+ });
195
+ }