@kal-elsam/kairo-runtime 0.2.2 → 0.2.3

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 CHANGED
@@ -24,13 +24,24 @@ commands) without depending on Pi as a runtime or adding a Pi adapter.
24
24
 
25
25
  ## Quick start
26
26
 
27
- Recommended entry — run Kairo Runtime in your terminal (interactive setup wizard in a TTY):
27
+ Recommended entry — run Kairo Runtime in your terminal:
28
28
 
29
29
  ```bash
30
30
  npx @kal-elsam/kairo-runtime
31
+ # or, after a global install:
32
+ kairo
31
33
  ```
32
34
 
33
- Preview without writing anything:
35
+ **First run** (no `~/.harness/state.json`): interactive onboarding → safe diagnosis →
36
+ setup with confirmation → operations dashboard.
37
+
38
+ **Later runs** (state present): operations dashboard with a stable purpose line and a
39
+ contextual next step (configure, enable intelligence, launch a run, or review problems).
40
+
41
+ Explicit commands and setup flags keep their current behavior (`kairo setup`,
42
+ `kairo --dry-run`, `kairo shell`, non-TTY scripts, etc.).
43
+
44
+ Preview setup without writing anything:
34
45
 
35
46
  ```bash
36
47
  npx @kal-elsam/kairo-runtime --dry-run
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kal-elsam/kairo-runtime",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Kairo Runtime — local agent operating system for Codex, Cursor, Claude, Gemini, Copilot, Engram, and Graphify.",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/Kal-elSam/harness#readme",
package/src/cli.js CHANGED
@@ -42,6 +42,11 @@ import {
42
42
  resolveSuggestedInvocation
43
43
  } from "./global/brand/cli.js";
44
44
  import { BRAND } from "./global/brand/index.js";
45
+ import {
46
+ INITIAL_EXPERIENCE,
47
+ hasConfiguredGlobalState,
48
+ resolveInitialExperience
49
+ } from "./global/initial-experience.js";
45
50
 
46
51
  export { resolveSuggestedInvocation };
47
52
 
@@ -50,7 +55,7 @@ const packageRoot = resolve(__dirname, "..");
50
55
  const SCOPES = new Set(["agent-global", "workspace"]);
51
56
 
52
57
  export async function runCli(argv) {
53
- const { command, options } = parseArgs(argv);
58
+ const { command, options, isImplicitCommand } = parseArgs(argv);
54
59
  maybeWarnLegacyCli(process.argv, { json: options.json });
55
60
 
56
61
  if (options.help || command === "help") {
@@ -69,14 +74,22 @@ export async function runCli(argv) {
69
74
  const invoke = resolveSuggestedInvocation(packageManifest.name);
70
75
 
71
76
  switch (command) {
72
- case "shell":
77
+ case "shell": {
78
+ const homeDir = resolveHomeDir();
79
+ const resolvedMode = resolveInitialExperience({
80
+ interactive: optionsWithPolicy.interactive,
81
+ isImplicitCommand,
82
+ hasGlobalState: hasConfiguredGlobalState(homeDir)
83
+ });
73
84
  await runOrchestratorShell({
74
85
  packageRoot,
75
86
  packageManifest,
76
87
  workspaceRoot: optionsWithPolicy.cwd,
77
- interactive: optionsWithPolicy.interactive
88
+ interactive: optionsWithPolicy.interactive,
89
+ initialMode: resolvedMode ?? INITIAL_EXPERIENCE.DASHBOARD
78
90
  });
79
91
  return;
92
+ }
80
93
  case "orchestrator":
81
94
  await runOrchestratorDiagnostics({
82
95
  homeDir: resolveHomeDir(),
@@ -468,7 +481,7 @@ export function parseArgs(argv) {
468
481
  options.task = args.join(" ").trim();
469
482
  }
470
483
 
471
- return { command, options };
484
+ return { command, options, isImplicitCommand: implicitCommand };
472
485
  }
473
486
 
474
487
  function parseComponentsAction(args, options) {
@@ -683,7 +696,8 @@ sections, components, backups, and drift repair under ~/.harness.
683
696
  Bootstrap: see README.md (curl install.sh or npx ${PACKAGE_NAME}).
684
697
 
685
698
  Usage:
686
- ${cli} Interactive orchestrator shell (TTY)
699
+ ${cli} First run: onboarding → setup → dashboard (TTY).
700
+ Later: operations dashboard with next-step guidance.
687
701
  ${cli} --dry-run Setup dry-run (scriptable)
688
702
  ${cli} --version
689
703
  ${cli} shell Operations dashboard (TTY)
@@ -727,7 +741,8 @@ Scopes:
727
741
  Explicit --scope=workspace only.
728
742
 
729
743
  Commands:
730
- shell Operations dashboard (TTY). Bare ${cli} opens this in TTY sessions.
744
+ shell Operations dashboard (TTY). Bare ${cli} opens onboarding when ~/.harness/state.json
745
+ is missing, otherwise the dashboard. Explicit ${cli} shell always opens the dashboard.
731
746
  run Launch a managed agent run with local audit trail.
732
747
  runs List, inspect, or cancel agent runs under ~/.harness/runs/.
733
748
  orchestrator Read-only capability registry diagnostics (--json supported).
@@ -70,6 +70,16 @@ export const WIZARD_COPY = {
70
70
  coreOnlyLabel: "Core only (no components)"
71
71
  };
72
72
 
73
+ /** First-run framing reused by onboarding → setup. */
74
+ export const ONBOARDING_COPY = {
75
+ welcomeTitle: `Welcome to ${BRAND.displayName}`,
76
+ purpose:
77
+ `${BRAND.displayName} detects, configures, and coordinates the local agents you already use.`,
78
+ safety:
79
+ "Diagnosis is read-only. Nothing is modified until you confirm a plan.",
80
+ continueHint: "Press Enter to diagnose and configure · Esc to exit"
81
+ };
82
+
73
83
  export function getAgentLabel(agentId) {
74
84
  return AGENT_LABELS[agentId] ?? agentId;
75
85
  }
@@ -0,0 +1,66 @@
1
+ import { formatCliCommand } from "./brand/cli.js";
2
+
3
+ export const DASHBOARD_PURPOSE =
4
+ "Detects, configures, and coordinates local AI agents — no changes without confirmation.";
5
+
6
+ export const NEXT_STEP_KINDS = {
7
+ CONFIGURE: "configure",
8
+ ENABLE_INTELLIGENCE: "enable_intelligence",
9
+ LAUNCH: "launch",
10
+ REVIEW: "review"
11
+ };
12
+
13
+ export function formatDashboardPurpose() {
14
+ return DASHBOARD_PURPOSE;
15
+ }
16
+
17
+ /**
18
+ * Contextual next step from existing diagnostics + dashboard snapshot.
19
+ * Priority: configure → review problems → enable intelligence → launch.
20
+ */
21
+ export function resolveDashboardRecommendation({
22
+ hasGlobalState = false,
23
+ diagnostics = null,
24
+ dashboard = null
25
+ } = {}) {
26
+ const summary = diagnostics?.diagnostics ?? { detected: 0, errors: 0 };
27
+ const intelligence = diagnostics?.intelligence?.summary;
28
+ const launchableCount = (dashboard?.providers ?? []).filter((entry) => entry.launchable).length;
29
+ const hasErrors = (summary.errors ?? 0) > 0;
30
+ const hasProblemRecommendation = (diagnostics?.recommendations ?? []).some((line) =>
31
+ /error|fix|drift|not detected|failed|problem/i.test(line)
32
+ );
33
+
34
+ if (!hasGlobalState || (summary.detected ?? 0) === 0) {
35
+ return {
36
+ kind: NEXT_STEP_KINDS.CONFIGURE,
37
+ message: `Configure the local environment with ${formatCliCommand("setup")}.`
38
+ };
39
+ }
40
+
41
+ if (hasErrors || hasProblemRecommendation) {
42
+ return {
43
+ kind: NEXT_STEP_KINDS.REVIEW,
44
+ message: "Review diagnostics for problems before launching a run."
45
+ };
46
+ }
47
+
48
+ if (!intelligence?.localAvailable && !intelligence?.cloudAuthenticated) {
49
+ return {
50
+ kind: NEXT_STEP_KINDS.ENABLE_INTELLIGENCE,
51
+ message: "Enable intelligence: start Ollama or set OPENROUTER_API_KEY, then retry."
52
+ };
53
+ }
54
+
55
+ if (launchableCount > 0) {
56
+ return {
57
+ kind: NEXT_STEP_KINDS.LAUNCH,
58
+ message: "Launch a supervised run from the menu or with kairo run."
59
+ };
60
+ }
61
+
62
+ return {
63
+ kind: NEXT_STEP_KINDS.REVIEW,
64
+ message: "Review diagnostics for problems before launching a run."
65
+ };
66
+ }
@@ -0,0 +1,34 @@
1
+ import { existsSync } from "node:fs";
2
+ import { harnessHomePaths } from "./paths.js";
3
+
4
+ export const INITIAL_EXPERIENCE = {
5
+ ONBOARDING: "onboarding",
6
+ DASHBOARD: "dashboard"
7
+ };
8
+
9
+ /**
10
+ * First-run marker is ~/.harness/state.json only.
11
+ * profile.json does not participate in this decision.
12
+ */
13
+ export function hasConfiguredGlobalState(homeDir) {
14
+ return existsSync(harnessHomePaths(homeDir).statePath);
15
+ }
16
+
17
+ /**
18
+ * Pure resolver for the interactive bare-entry experience.
19
+ * Returns null when CLI should keep existing non-onboarding paths
20
+ * (non-TTY, explicit commands, setup flags already routed elsewhere).
21
+ */
22
+ export function resolveInitialExperience({
23
+ interactive = false,
24
+ isImplicitCommand = false,
25
+ hasGlobalState = false
26
+ } = {}) {
27
+ if (!interactive || !isImplicitCommand) {
28
+ return null;
29
+ }
30
+
31
+ return hasGlobalState
32
+ ? INITIAL_EXPERIENCE.DASHBOARD
33
+ : INITIAL_EXPERIENCE.ONBOARDING;
34
+ }
@@ -27,6 +27,10 @@ import {
27
27
  selectRunFromList,
28
28
  shiftMenuIndex
29
29
  } from "./orchestrator-state.js";
30
+ import {
31
+ formatDashboardPurpose,
32
+ resolveDashboardRecommendation
33
+ } from "../dashboard-guidance.js";
30
34
 
31
35
  const COLORS = {
32
36
  accent: "cyan",
@@ -42,6 +46,7 @@ export function OrchestratorApp({
42
46
  packageName,
43
47
  packageRoot,
44
48
  cliVersion,
49
+ hasGlobalState = false,
45
50
  onComplete
46
51
  }) {
47
52
  const { exit } = useApp();
@@ -361,13 +366,14 @@ export function OrchestratorApp({
361
366
 
362
367
  return React.createElement(Box, { flexDirection: "column" },
363
368
  React.createElement(Text, { bold: true, color: COLORS.accent }, `${BRAND.displayName} runtime`),
364
- React.createElement(Text, { color: COLORS.muted }, "Launch · supervise · audit agent runs"),
369
+ React.createElement(Text, { color: COLORS.muted }, formatDashboardPurpose()),
365
370
  statusMessage && React.createElement(Text, { color: COLORS.success }, statusMessage),
366
371
  React.createElement(Text, null, ""),
367
372
  renderView({
368
373
  view,
369
374
  dashboard,
370
375
  diagnostics,
376
+ hasGlobalState,
371
377
  menuIndex,
372
378
  listIndex,
373
379
  launchStep,
@@ -387,6 +393,7 @@ function renderView({
387
393
  view,
388
394
  dashboard,
389
395
  diagnostics,
396
+ hasGlobalState,
390
397
  menuIndex,
391
398
  listIndex,
392
399
  launchStep,
@@ -398,8 +405,16 @@ function renderView({
398
405
  selectedEvents
399
406
  }) {
400
407
  switch (view) {
401
- case ORCHESTRATOR_VIEWS.HOME:
408
+ case ORCHESTRATOR_VIEWS.HOME: {
409
+ const nextStep = resolveDashboardRecommendation({
410
+ hasGlobalState,
411
+ diagnostics,
412
+ dashboard
413
+ });
402
414
  return React.createElement(Box, { flexDirection: "column" },
415
+ React.createElement(Text, { bold: true }, "Next"),
416
+ React.createElement(Text, { color: COLORS.accent }, nextStep.message),
417
+ React.createElement(Text, null, ""),
403
418
  React.createElement(Text, { bold: true }, "Operations"),
404
419
  ORCHESTRATOR_MENU.map((item, index) =>
405
420
  React.createElement(Text, {
@@ -413,6 +428,7 @@ function renderView({
413
428
  formatDashboardSnapshot(dashboard)
414
429
  .map((line) => React.createElement(Text, { key: line }, line))
415
430
  );
431
+ }
416
432
  case ORCHESTRATOR_VIEWS.ACTIVE_RUNS:
417
433
  return React.createElement(Box, { flexDirection: "column" },
418
434
  React.createElement(Text, { bold: true }, "Active runs"),
@@ -8,6 +8,7 @@ export async function runOrchestratorInk({
8
8
  packageRoot,
9
9
  packageName,
10
10
  cliVersion,
11
+ hasGlobalState = false,
11
12
  renderImpl = render
12
13
  }) {
13
14
  return new Promise((resolve) => {
@@ -18,6 +19,7 @@ export async function runOrchestratorInk({
18
19
  packageRoot,
19
20
  packageName,
20
21
  cliVersion,
22
+ hasGlobalState,
21
23
  onComplete: resolve
22
24
  })
23
25
  );
@@ -15,6 +15,7 @@ export async function runSetupInk({
15
15
  packageName,
16
16
  cliVersion,
17
17
  dryRun = false,
18
+ onboarding = false,
18
19
  preflight = true,
19
20
  yes = false,
20
21
  confirm = false,
@@ -33,6 +34,7 @@ export async function runSetupInk({
33
34
  packageName,
34
35
  cliVersion,
35
36
  dryRun,
37
+ onboarding,
36
38
  onComplete: resolve
37
39
  })
38
40
  );
@@ -56,8 +56,8 @@ function Footer({ children }) {
56
56
  return React.createElement(Text, { dimColor: true }, children);
57
57
  }
58
58
 
59
- function Splash({ compact }) {
60
- const lines = formatInkSplashLines({ compact });
59
+ function Splash({ compact, onboarding = false }) {
60
+ const lines = formatInkSplashLines({ compact, onboarding });
61
61
  const logoLineCount = compact ? BRAND.compactLogo.length : BRAND.asciiLogo.length;
62
62
 
63
63
  return React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
@@ -71,7 +71,7 @@ function Splash({ compact }) {
71
71
  if (line === BRAND.tagline) {
72
72
  return React.createElement(Text, { key: `line-${index}`, color: INK_COLORS.muted }, line);
73
73
  }
74
- if (line === BRAND.splashHint) {
74
+ if (line === BRAND.splashHint || line.includes("Esc to exit") || line.includes("Press Enter")) {
75
75
  return React.createElement(Text, { key: `line-${index}`, dimColor: true }, line);
76
76
  }
77
77
  if (line === "") {
@@ -89,6 +89,7 @@ export function SetupApp({
89
89
  packageName,
90
90
  cliVersion,
91
91
  dryRun = false,
92
+ onboarding = false,
92
93
  onComplete
93
94
  }) {
94
95
  const { exit } = useApp();
@@ -243,7 +244,10 @@ export function SetupApp({
243
244
  const detectPanel = formatInkDetectPanel({ adapters, detected });
244
245
 
245
246
  return React.createElement(Box, { flexDirection: "column" },
246
- step === SETUP_STEPS.SPLASH && React.createElement(Splash, { compact: useCompactSplash }),
247
+ step === SETUP_STEPS.SPLASH && React.createElement(Splash, {
248
+ compact: useCompactSplash,
249
+ onboarding
250
+ }),
247
251
  step !== SETUP_STEPS.SPLASH && React.createElement(Header),
248
252
  step === SETUP_STEPS.DETECT && React.createElement(Panel, { title: WIZARD_COPY.detectTitle },
249
253
  detectPanel.split("\n")
@@ -1,4 +1,12 @@
1
- import { AGENT_HINTS, BRAND, formatCliCommand, getAgentLabel, PREFERRED_CLI, WIZARD_COPY } from "../brand/index.js";
1
+ import {
2
+ AGENT_HINTS,
3
+ BRAND,
4
+ ONBOARDING_COPY,
5
+ formatCliCommand,
6
+ getAgentLabel,
7
+ PREFERRED_CLI,
8
+ WIZARD_COPY
9
+ } from "../brand/index.js";
2
10
  import { formatAgentMultiselectHint } from "../clack/theme.js";
3
11
 
4
12
  export const SETUP_STEPS = {
@@ -16,8 +24,22 @@ export function shouldUseCompactSplashLogo(columns) {
16
24
  return columns < fullWidth + 4;
17
25
  }
18
26
 
19
- export function formatInkSplashLines({ compact = false } = {}) {
27
+ export function formatInkSplashLines({ compact = false, onboarding = false } = {}) {
20
28
  const logo = compact ? BRAND.compactLogo : BRAND.asciiLogo;
29
+ if (onboarding) {
30
+ return [
31
+ ...logo,
32
+ "",
33
+ BRAND.name,
34
+ BRAND.tagline,
35
+ "",
36
+ ONBOARDING_COPY.purpose,
37
+ ONBOARDING_COPY.safety,
38
+ "",
39
+ ONBOARDING_COPY.continueHint
40
+ ];
41
+ }
42
+
21
43
  return [
22
44
  ...logo,
23
45
  "",
@@ -5,6 +5,11 @@ import { runOrchestratorInk as defaultRunOrchestratorInk } from "./ink/run-orche
5
5
  import { formatCliCommand } from "./brand/cli.js";
6
6
  import { BRAND } from "./brand/index.js";
7
7
  import { buildReadOnlyDiagnostics, shouldExecutePlan } from "./action-planner.js";
8
+ import { runHarnessSetup as defaultRunHarnessSetup } from "./setup.js";
9
+ import {
10
+ INITIAL_EXPERIENCE,
11
+ hasConfiguredGlobalState
12
+ } from "./initial-experience.js";
8
13
 
9
14
  export { canUseOrchestratorShell };
10
15
 
@@ -23,7 +28,10 @@ export async function runOrchestratorShell({
23
28
  packageManifest,
24
29
  workspaceRoot,
25
30
  interactive = Boolean(input.isTTY && output.isTTY),
26
- runOrchestratorInkImpl = defaultRunOrchestratorInk
31
+ initialMode = INITIAL_EXPERIENCE.DASHBOARD,
32
+ shellCapable = canUseOrchestratorShell({ interactive }),
33
+ runOrchestratorInkImpl = defaultRunOrchestratorInk,
34
+ runHarnessSetupImpl = defaultRunHarnessSetup
27
35
  }) {
28
36
  if (!interactive) {
29
37
  throw new Error(
@@ -31,19 +39,44 @@ export async function runOrchestratorShell({
31
39
  );
32
40
  }
33
41
 
34
- if (!canUseOrchestratorShell({ interactive })) {
42
+ if (!shellCapable) {
35
43
  throw new Error(
36
44
  `Interactive shell requires a capable TTY. Use ${formatCliCommand("runs list")} or explicit commands.`
37
45
  );
38
46
  }
39
47
 
40
48
  const homeDir = resolveHomeDir();
49
+ let setupOutcome = null;
50
+
51
+ if (initialMode === INITIAL_EXPERIENCE.ONBOARDING) {
52
+ setupOutcome = await runHarnessSetupImpl({
53
+ packageRoot,
54
+ packageName: packageManifest.name,
55
+ cliVersion: packageManifest.version,
56
+ homeDir,
57
+ workspaceRoot,
58
+ onboarding: true,
59
+ interactive: true
60
+ });
61
+
62
+ if (setupOutcome?.cancelled) {
63
+ return {
64
+ cancelled: true,
65
+ wrote: false,
66
+ action: null,
67
+ initialMode,
68
+ setup: setupOutcome
69
+ };
70
+ }
71
+ }
72
+
41
73
  const outcome = await runOrchestratorInkImpl({
42
74
  homeDir,
43
75
  workspaceRoot,
44
76
  packageRoot,
45
77
  packageName: packageManifest.name,
46
- cliVersion: packageManifest.version
78
+ cliVersion: packageManifest.version,
79
+ hasGlobalState: hasConfiguredGlobalState(homeDir)
47
80
  });
48
81
 
49
82
  if (outcome.error) {
@@ -52,8 +85,10 @@ export async function runOrchestratorShell({
52
85
 
53
86
  return {
54
87
  cancelled: Boolean(outcome.cancelled),
55
- wrote: false,
56
- action: outcome.action ?? null
88
+ wrote: Boolean(setupOutcome && !setupOutcome.cancelled),
89
+ action: outcome.action ?? null,
90
+ initialMode,
91
+ setup: setupOutcome
57
92
  };
58
93
  }
59
94
 
@@ -1,5 +1,5 @@
1
1
  import { stdin as input, stdout as output } from "node:process";
2
- import { BRAND } from "./brand/index.js";
2
+ import { BRAND, ONBOARDING_COPY } from "./brand/index.js";
3
3
  import { installGlobalHarness } from "./global-installer.js";
4
4
  import {
5
5
  assertExplicitApplyConsent,
@@ -59,6 +59,7 @@ export async function runHarnessSetup({
59
59
  confirmExplicit = false,
60
60
  json = false,
61
61
  simple = false,
62
+ onboarding = false,
62
63
  interactive = Boolean(input.isTTY && output.isTTY),
63
64
  createPrompt = createReadlinePrompt,
64
65
  runSetupInkImpl = defaultRunSetupInk,
@@ -76,7 +77,7 @@ export async function runHarnessSetup({
76
77
  let usedInk = false;
77
78
 
78
79
  if (!useInk && !useWizard) {
79
- printSetupIntro({ homeDir });
80
+ printSetupIntro({ homeDir, onboarding });
80
81
  }
81
82
 
82
83
  const setupUiArgs = {
@@ -86,6 +87,7 @@ export async function runHarnessSetup({
86
87
  packageName,
87
88
  cliVersion,
88
89
  dryRun,
90
+ onboarding,
89
91
  preflight,
90
92
  yes,
91
93
  confirm,
@@ -254,11 +256,17 @@ export async function runHarnessSetup({
254
256
  return { cancelled: false, result, usedWizard, usedInk };
255
257
  }
256
258
 
257
- function printSetupIntro({ homeDir }) {
259
+ function printSetupIntro({ homeDir, onboarding = false }) {
258
260
  const detected = detectInstalledAdapters({ homeDir });
259
261
 
260
- console.log(`${BRAND.displayName} setup — local AI ecosystem configurator`);
261
- console.log("Configures and coordinates local agents. Does not install the AI apps themselves.");
262
+ if (onboarding) {
263
+ console.log(ONBOARDING_COPY.welcomeTitle);
264
+ console.log(ONBOARDING_COPY.purpose);
265
+ console.log(ONBOARDING_COPY.safety);
266
+ } else {
267
+ console.log(`${BRAND.displayName} setup — local AI ecosystem configurator`);
268
+ console.log("Configures and coordinates local agents. Does not install the AI apps themselves.");
269
+ }
262
270
  console.log("");
263
271
  console.log(`Detected agents: ${detected.join(", ") || "none"}`);
264
272
  console.log(`Supported agents: ${GLOBAL_AGENT_IDS.join(", ")}`);