trucontext 0.5.1 → 0.7.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')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trucontext",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
4
4
  "description": "TruContext CLI — contextual memory for AI applications",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,5 +1,6 @@
1
1
  import chalk from 'chalk';
2
- import { dataPlane } from '../client.js';
2
+ import { controlPlane } from '../client.js';
3
+ import { getActiveApp } from '../config.js';
3
4
 
4
5
  // Fields stored on the node but not user-declared properties
5
6
  const SYSTEM_FIELDS = new Set([
@@ -38,15 +39,16 @@ function displayEntity(e) {
38
39
 
39
40
  export async function entitiesListCommand(options) {
40
41
  try {
42
+ const appId = getActiveApp();
41
43
  const params = new URLSearchParams();
42
44
  if (options.type) params.set('type', options.type);
43
45
  if (options.context) params.set('context_id', options.context);
44
46
  if (options.scope) params.set('scope', options.scope);
45
47
  if (options.limit) params.set('limit', options.limit);
46
48
  const qs = params.toString();
47
- const path = `/v1/entities${qs ? `?${qs}` : ''}`;
49
+ const path = `/apps/${appId}/entities${qs ? `?${qs}` : ''}`;
48
50
 
49
- const res = await dataPlane('GET', path);
51
+ const res = await controlPlane('GET', path);
50
52
  const entities = Array.isArray(res.data) ? res.data : (res.data?.entities || []);
51
53
 
52
54
  if (entities.length === 0) {
@@ -66,13 +68,14 @@ export async function entitiesListCommand(options) {
66
68
 
67
69
  export async function entitiesGetCommand(entityId) {
68
70
  try {
69
- const res = await dataPlane('GET', `/v1/entities/${entityId}`);
71
+ const appId = getActiveApp();
72
+ const res = await controlPlane('GET', `/apps/${appId}/entities/${entityId}`);
70
73
  const e = res.data || res;
71
74
  displayEntity(e);
72
75
 
73
76
  // Show edges if available
74
77
  try {
75
- const edgeRes = await dataPlane('GET', `/v1/entities/${entityId}/edges`);
78
+ const edgeRes = await controlPlane('GET', `/apps/${appId}/entities/${entityId}/edges`);
76
79
  const edges = edgeRes.data?.edges || edgeRes.data || [];
77
80
  if (edges.length > 0) {
78
81
  console.log(chalk.dim(`\n Edges (${edges.length}):`));
@@ -101,6 +104,7 @@ export async function entitiesCreateCommand(options) {
101
104
  process.exit(1);
102
105
  }
103
106
 
107
+ const appId = getActiveApp();
104
108
  const body = {
105
109
  entityId: options.id,
106
110
  type: options.type,
@@ -133,7 +137,7 @@ export async function entitiesCreateCommand(options) {
133
137
  });
134
138
  }
135
139
 
136
- const res = await dataPlane('POST', '/v1/entities', body);
140
+ const res = await controlPlane('POST', `/apps/${appId}/entities`, body);
137
141
  const e = res.data || res;
138
142
  console.log(chalk.green(`Created: ${chalk.bold(e.entityId || options.id)}`));
139
143
  } catch (err) {
@@ -144,6 +148,7 @@ export async function entitiesCreateCommand(options) {
144
148
 
145
149
  export async function entitiesUpdateCommand(entityId, options) {
146
150
  try {
151
+ const appId = getActiveApp();
147
152
  const body = {};
148
153
 
149
154
  if (options.properties) {
@@ -173,7 +178,7 @@ export async function entitiesUpdateCommand(entityId, options) {
173
178
  });
174
179
  }
175
180
 
176
- await dataPlane('PUT', `/v1/entities/${entityId}`, body);
181
+ await controlPlane('PUT', `/apps/${appId}/entities/${entityId}`, body);
177
182
  console.log(chalk.green(`Updated: ${entityId}`));
178
183
  } catch (err) {
179
184
  console.error(chalk.red(`Failed: ${err.message}`));
@@ -183,7 +188,8 @@ export async function entitiesUpdateCommand(entityId, options) {
183
188
 
184
189
  export async function entitiesDeleteCommand(entityId) {
185
190
  try {
186
- await dataPlane('DELETE', `/v1/entities/${entityId}`);
191
+ const appId = getActiveApp();
192
+ await controlPlane('DELETE', `/apps/${appId}/entities/${entityId}`);
187
193
  console.log(chalk.green(`Deleted: ${entityId}`));
188
194
  } catch (err) {
189
195
  console.error(chalk.red(`Failed: ${err.message}`));
@@ -193,7 +199,8 @@ export async function entitiesDeleteCommand(entityId) {
193
199
 
194
200
  export async function entitiesEdgesCommand(entityId) {
195
201
  try {
196
- const res = await dataPlane('GET', `/v1/entities/${entityId}/edges`);
202
+ const appId = getActiveApp();
203
+ const res = await controlPlane('GET', `/apps/${appId}/entities/${entityId}/edges`);
197
204
  const edges = res.data?.edges || res.data || [];
198
205
 
199
206
  if (edges.length === 0) {
@@ -1,10 +1,12 @@
1
1
  import chalk from 'chalk';
2
2
  import { readFileSync, existsSync } from 'fs';
3
3
  import { basename } from 'path';
4
- import { dataPlane } from '../client.js';
4
+ import { controlPlane } from '../client.js';
5
+ import { getActiveApp } from '../config.js';
5
6
 
6
7
  export async function ingestCommand(source, options) {
7
8
  try {
9
+ const appId = getActiveApp();
8
10
  let body;
9
11
 
10
12
  if (existsSync(source)) {
@@ -32,25 +34,30 @@ export async function ingestCommand(source, options) {
32
34
  if (options.confidence !== undefined) body.confidence = parseFloat(options.confidence);
33
35
  if (options.temporal !== undefined) body.temporal = options.temporal;
34
36
 
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
- });
37
+ // Build contexts array from --context flags: "entity-id:RELATIONSHIP" (required)
38
+ if (!options.context || options.context.length === 0) {
39
+ console.error(chalk.red('At least one context is required.'));
40
+ console.error(chalk.dim('Use: --context entity-id:RELATIONSHIP (e.g., --context dustin:ABOUT)'));
41
+ process.exit(1);
47
42
  }
48
43
 
49
- const res = await dataPlane('POST', '/v1/ingest', body);
44
+ body.contexts = options.context.map(entry => {
45
+ const colonIdx = entry.indexOf(':');
46
+ if (colonIdx === -1) {
47
+ return { context_id: entry, relationship: 'ABOUT' };
48
+ }
49
+ return {
50
+ context_id: entry.slice(0, colonIdx),
51
+ relationship: entry.slice(colonIdx + 1),
52
+ };
53
+ });
54
+
55
+ const res = await controlPlane('POST', `/apps/${appId}/ingest`, body);
56
+ const data = res.data || res;
50
57
  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}`));
58
+ console.log(chalk.dim(`Content ID: ${data.contentId}`));
59
+ if (data.status === 'upload_required') {
60
+ console.log(chalk.yellow(`Large file — upload to: ${data.uploadUrl}`));
54
61
  }
55
62
  } catch (err) {
56
63
  console.error(chalk.red(`Ingest failed: ${err.message}`));
@@ -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', `/apps/${app.appId}/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,21 @@
1
1
  import chalk from 'chalk';
2
- import { dataPlane } from '../client.js';
2
+ import { controlPlane } from '../client.js';
3
+ import { getConfig, getActiveApp } from '../config.js';
3
4
 
4
5
  export async function queryCommand(question, options) {
5
6
  try {
7
+ const appId = getActiveApp();
8
+
9
+ // root_id is required — from --root flag or active root
10
+ const rootId = options.root || getConfig().activeRoot;
11
+ if (!rootId) {
12
+ console.error(chalk.red('Root node required. Use --root <id> or set active root: trucontext roots use <id>'));
13
+ process.exit(1);
14
+ }
15
+
6
16
  const body = {
7
17
  mode: options.mode === 'context' ? 'CONTEXT' : 'ANSWER',
18
+ root_id: rootId,
8
19
  max_results: options.limit ? parseInt(options.limit, 10) : 20,
9
20
  };
10
21
 
@@ -16,34 +27,35 @@ export async function queryCommand(question, options) {
16
27
 
17
28
  if (options.context) body.context_id = options.context;
18
29
 
19
- const res = await dataPlane('POST', '/v1/query', body);
30
+ const res = await controlPlane('POST', `/apps/${appId}/mind/query`, body);
31
+ const data = res.data || res;
20
32
 
21
33
  // Print answer
22
- if (res.answer?.summary) {
34
+ if (data.answer?.summary) {
23
35
  console.log(chalk.bold('\nAnswer:'));
24
- console.log(res.answer.summary);
25
- if (res.answer.confidence) {
26
- console.log(chalk.dim(`\nConfidence: ${(res.answer.confidence * 100).toFixed(0)}%`));
36
+ console.log(data.answer.summary);
37
+ if (data.answer.confidence) {
38
+ console.log(chalk.dim(`\nConfidence: ${(data.answer.confidence * 100).toFixed(0)}%`));
27
39
  }
28
40
  }
29
41
 
30
42
  // Print concepts
31
- if (res.concepts?.length > 0) {
43
+ if (data.concepts?.length > 0) {
32
44
  console.log(chalk.bold('\nConcepts:'));
33
- for (const c of res.concepts.slice(0, 5)) {
45
+ for (const c of data.concepts.slice(0, 5)) {
34
46
  console.log(` ${c.label} ${chalk.dim(`(${c.evidence_count} evidence)`)}`);
35
47
  }
36
48
  }
37
49
 
38
50
  // Print people
39
- if (res.people?.length > 0) {
51
+ if (data.people?.length > 0) {
40
52
  console.log(chalk.bold('\nPeople:'));
41
- for (const p of res.people.slice(0, 5)) {
53
+ for (const p of data.people.slice(0, 5)) {
42
54
  console.log(` ${p.name} ${chalk.dim(p.relationship || '')}`);
43
55
  }
44
56
  }
45
57
 
46
- console.log(chalk.dim(`\n${res.latency_ms}ms`));
58
+ console.log(chalk.dim(`\n${data.latency_ms}ms`));
47
59
  } catch (err) {
48
60
  console.error(chalk.red(`Query failed: ${err.message}`));
49
61
  process.exit(1);
@@ -1,28 +1,40 @@
1
1
  import chalk from 'chalk';
2
- import { dataPlane } from '../client.js';
2
+ import { controlPlane } from '../client.js';
3
+ import { getConfig, getActiveApp } from '../config.js';
3
4
 
4
5
  export async function recallCommand(query, options) {
5
6
  try {
7
+ const appId = getActiveApp();
8
+
9
+ // root_id is required — from --root flag or active root
10
+ const rootId = options.root || getConfig().activeRoot;
11
+ if (!rootId) {
12
+ console.error(chalk.red('Root node required. Use --root <id> or set active root: trucontext roots use <id>'));
13
+ process.exit(1);
14
+ }
15
+
6
16
  const body = {
7
17
  query,
18
+ root_id: rootId,
8
19
  maxResults: options.limit ? parseInt(options.limit, 10) : 10,
9
20
  expansionDepth: options.depth ? parseInt(options.depth, 10) : 2,
10
21
  };
11
22
 
12
23
  if (options.context) body.context_id = options.context;
13
24
 
14
- const res = await dataPlane('POST', '/v1/recall', body);
25
+ const res = await controlPlane('POST', `/apps/${appId}/recall`, body);
26
+ const data = res.data || res;
15
27
 
16
28
  // Print synthesis
17
- if (res.synthesis?.summary) {
29
+ if (data.synthesis?.summary) {
18
30
  console.log(chalk.bold('\nSynthesis:'));
19
- console.log(res.synthesis.summary);
31
+ console.log(data.synthesis.summary);
20
32
  }
21
33
 
22
34
  // Print seeds
23
- if (res.seeds?.length > 0) {
24
- console.log(chalk.bold(`\nSeeds (${res.seeds.length}):`));
25
- for (const s of res.seeds) {
35
+ if (data.seeds?.length > 0) {
36
+ console.log(chalk.bold(`\nSeeds (${data.seeds.length}):`));
37
+ for (const s of data.seeds) {
26
38
  const label = s.snippet || s.label || s.id;
27
39
  const score = (s.score * 100).toFixed(0);
28
40
  console.log(` ${chalk.dim(`${score}%`)} ${label.slice(0, 100)}`);
@@ -30,11 +42,11 @@ export async function recallCommand(query, options) {
30
42
  }
31
43
 
32
44
  // Print graph context summary
33
- if (res.context) {
34
- console.log(chalk.dim(`\nGraph: ${res.context.nodes?.length || 0} nodes, ${res.context.edges?.length || 0} edges`));
45
+ if (data.context) {
46
+ console.log(chalk.dim(`\nGraph: ${data.context.nodes?.length || 0} nodes, ${data.context.edges?.length || 0} edges`));
35
47
  }
36
48
 
37
- console.log(chalk.dim(`${res.latency_ms}ms`));
49
+ console.log(chalk.dim(`${data.latency_ms}ms`));
38
50
  } catch (err) {
39
51
  console.error(chalk.red(`Recall failed: ${err.message}`));
40
52
  process.exit(1);
@@ -0,0 +1,201 @@
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, getActiveApp } 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 appId = getActiveApp();
58
+ const res = await controlPlane('POST', `/apps/${appId}/roots`, body);
59
+ const r = res.data;
60
+ console.log(chalk.green(`Root node created: ${r.entityId}`));
61
+ console.log(` Type: ${r.type}`);
62
+ console.log(` Recipe: ${r.recipe_id}`);
63
+ console.log(` Dreamers: ${Array.isArray(r.dreamers) ? r.dreamers.join(', ') : r.dreamers}`);
64
+ console.log(` Hop depth: ${r.hop_depth || 'unlimited'}`);
65
+ } catch (err) {
66
+ console.error(chalk.red('Failed:'), err.response?.data?.error || err.response?.data?.details || err.message);
67
+ process.exit(1);
68
+ }
69
+ });
70
+
71
+ // LIST
72
+ roots
73
+ .command('list')
74
+ .description('List all root nodes for the active app')
75
+ .action(async () => {
76
+ try {
77
+ const appId = getActiveApp();
78
+ const res = await controlPlane('GET', `/apps/${appId}/roots`);
79
+ const rootNodes = res.data?.roots || [];
80
+ if (rootNodes.length === 0) {
81
+ console.log('No root nodes. Create one with: trucontext roots create');
82
+ return;
83
+ }
84
+
85
+ const config = getConfig();
86
+ for (const r of rootNodes) {
87
+ const active = config.activeRoot === r.entityId ? chalk.cyan(' (active)') : '';
88
+ const dreamers = Array.isArray(r.dreamers) ? r.dreamers.join(', ') : r.dreamers;
89
+ console.log(`${chalk.bold(r.properties?.name || r.entityId)}${active}`);
90
+ console.log(` ${r.entityId} — ${r.type} — recipe: ${r.recipe_id || 'none'} — dreamers: ${dreamers}`);
91
+ }
92
+ } catch (err) {
93
+ console.error(chalk.red('Failed:'), err.response?.data?.error || err.message);
94
+ process.exit(1);
95
+ }
96
+ });
97
+
98
+ // SHOW
99
+ roots
100
+ .command('show <rootId>')
101
+ .description('Show details of a root node')
102
+ .action(async (rootId) => {
103
+ try {
104
+ const appId = getActiveApp();
105
+ const res = await controlPlane('GET', `/apps/${appId}/roots/${rootId}`);
106
+ const r = res.data;
107
+ console.log(chalk.bold(`Root: ${r.entityId}`));
108
+ console.log(` Type: ${r.type}`);
109
+ console.log(` Recipe: ${r.recipe_id || 'none'}`);
110
+ console.log(` Dreamers: ${Array.isArray(r.dreamers) ? r.dreamers.join(', ') : r.dreamers}`);
111
+ console.log(` Hop depth: ${r.hop_depth || 'unlimited'}`);
112
+ console.log(` Do not follow: ${r.do_not_follow}`);
113
+
114
+ if (r.linkedRoots && r.linkedRoots.length > 0) {
115
+ console.log(` Linked roots:`);
116
+ for (const lr of r.linkedRoots) {
117
+ console.log(` → ${lr.entityId} (${lr.type}) via ${lr.relationship}`);
118
+ }
119
+ }
120
+ } catch (err) {
121
+ console.error(chalk.red('Failed:'), err.response?.data?.error || err.message);
122
+ process.exit(1);
123
+ }
124
+ });
125
+
126
+ // UPDATE
127
+ roots
128
+ .command('update <rootId>')
129
+ .description('Update a root node')
130
+ .option('--recipe <recipeId>', 'New recipe ID')
131
+ .option('--dreamers <list>', 'New dreamers: "all" or comma-separated')
132
+ .option('--hop-depth <n>', 'New hop depth', parseInt)
133
+ .option('--do-not-follow', 'Set do-not-follow flag')
134
+ .option('--no-do-not-follow', 'Clear do-not-follow flag')
135
+ .option('--properties <json>', 'JSON properties to update')
136
+ .action(async (rootId, opts) => {
137
+ try {
138
+ const body = {};
139
+ if (opts.recipe) body.recipe_id = opts.recipe;
140
+ if (opts.dreamers) body.dreamers = opts.dreamers === 'all' ? 'all' : opts.dreamers.split(',').map(s => s.trim());
141
+ if (opts.hopDepth !== undefined) body.hop_depth = opts.hopDepth;
142
+ if (opts.doNotFollow !== undefined) body.do_not_follow = opts.doNotFollow;
143
+ if (opts.properties) {
144
+ try { body.properties = JSON.parse(opts.properties); } catch {
145
+ console.error(chalk.red('Error: --properties must be valid JSON'));
146
+ process.exit(1);
147
+ }
148
+ }
149
+
150
+ const appId = getActiveApp();
151
+ await controlPlane('PUT', `/apps/${appId}/roots/${rootId}`, body);
152
+ console.log(chalk.green(`Updated: ${rootId}`));
153
+ } catch (err) {
154
+ console.error(chalk.red('Failed:'), err.response?.data?.error || err.message);
155
+ process.exit(1);
156
+ }
157
+ });
158
+
159
+ // DELETE (demote)
160
+ roots
161
+ .command('delete <rootId>')
162
+ .description('Demote a root node to a regular entity (does not delete the node)')
163
+ .action(async (rootId) => {
164
+ try {
165
+ const appId = getActiveApp();
166
+ await controlPlane('DELETE', `/apps/${appId}/roots/${rootId}`);
167
+ console.log(chalk.green(`Demoted: ${rootId} (no longer a root node)`));
168
+
169
+ const config = getConfig();
170
+ if (config.activeRoot === rootId) {
171
+ config.activeRoot = undefined;
172
+ saveConfig(config);
173
+ }
174
+ } catch (err) {
175
+ console.error(chalk.red('Failed:'), err.response?.data?.error || err.message);
176
+ process.exit(1);
177
+ }
178
+ });
179
+
180
+ // USE — set active root for queries
181
+ roots
182
+ .command('use <rootId>')
183
+ .description('Set the active root node for queries and recall')
184
+ .action(async (rootId) => {
185
+ try {
186
+ const appId = getActiveApp();
187
+ await controlPlane('GET', `/apps/${appId}/roots/${rootId}`);
188
+ const config = getConfig();
189
+ config.activeRoot = rootId;
190
+ saveConfig(config);
191
+ console.log(chalk.green(`Active root: ${rootId}`));
192
+ } catch (err) {
193
+ if (err.response?.status === 404) {
194
+ console.error(chalk.red(`Root node '${rootId}' not found. Run: trucontext roots list`));
195
+ } else {
196
+ console.error(chalk.red('Failed:'), err.response?.data?.error || err.message);
197
+ }
198
+ process.exit(1);
199
+ }
200
+ });
201
+ }