@iseoane/agent-depot 0.5.0 → 0.5.2

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.
@@ -1,9 +1,10 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
3
  import { describeSkills, HOST_CHOICES, SCOPE_CHOICES } from "./catalog-actions.js";
4
+ import { packageOwnedSkillNotice, packageOwnerBadge } from "./catalog-ownership.js";
4
5
  import { skillKey } from "./catalog-tree.js";
5
6
  import { hasUnmanagedCopy } from "./catalog-installs.js";
6
- import { describeRow, UNMANAGED_MARKER } from "./catalog-tree.js";
7
+ import { describeRow, truncateDescription, UNMANAGED_MARKER } from "./catalog-tree.js";
7
8
  import { isPackageMode, PackagePanel } from "./package-actions.js";
8
9
  import { HostChecklist, PreviewLines } from "./panel-parts.js";
9
10
  import { rowStyle } from "./theme.js";
@@ -17,10 +18,17 @@ export function CatalogRowLine({ row, selected, marked, installed }) {
17
18
  const { data } = row.node;
18
19
  const isGroup = data.kind === "source" || data.kind === "group";
19
20
  const ownedBy = data.kind === "skill" ? data.ownedBy : undefined;
20
- return (_jsxs(Text, { ...rowStyle(selected), color: isGroup ? theme.group : ownedBy === undefined ? undefined : theme.inactive, children: [selected ? "> " : " ", " ".repeat(row.depth), data.kind === "skill" ? marked ? "[x] " : "[ ] " : "", describeRow(row), ownedBy === undefined ? null : _jsxs(Text, { color: theme.inactive, children: [" ", "provided by the ", ownedBy.host, " Package ", ownedBy.name] }), data.kind === "skill" && hasUnmanagedCopy(data.skill, installed)
21
+ return (_jsxs(Text, { ...rowStyle(selected), color: isGroup ? theme.group : undefined, wrap: "truncate-end", children: [selected ? "> " : " ", " ".repeat(row.depth), data.kind === "skill" ? marked ? "[x] " : "[ ] " : "", data.kind === "skill" && ownedBy !== undefined
22
+ ? _jsxs(_Fragment, { children: [data.skill.name, _jsxs(Text, { color: theme.marker, children: [" ", "[", packageOwnerBadge(ownedBy), "]"] }), " ", truncateDescription(data.skill.description)] })
23
+ : describeRow(row), data.kind === "skill" && hasUnmanagedCopy(data.skill, installed)
21
24
  ? _jsxs(Text, { color: theme.marker, children: [" ", UNMANAGED_MARKER] })
22
25
  : null] }));
23
26
  }
27
+ /** Full Package ownership reason for the highlighted Skill, outside the one-line row. */
28
+ export function CatalogSelectionHelp({ row, height }) {
29
+ const owner = row?.node.data.kind === "skill" ? row.node.data.ownedBy : undefined;
30
+ return (_jsx(Box, { flexDirection: "column", minHeight: height, children: owner === undefined ? null : _jsx(Text, { color: theme.marker, children: packageOwnedSkillNotice(owner) }) }));
31
+ }
24
32
  /** The prompt of the install flow; browsing shows nothing. */
25
33
  export function ActionPanel({ mode }) {
26
34
  if (isPackageMode(mode))
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useEffect, useReducer, useRef, useState } from "react";
2
- import { packageSelectionFor } from "../package-model.js";
2
+ import { packageSelectionFor, selectionKey } from "../package-model.js";
3
3
  import { projectSourceOf } from "../sources.js";
4
4
  import { filterReducer, initialFilter } from "./catalog-filter.js";
5
5
  import { loadInstalledSkills, NO_INSTALLED } from "./catalog-installs.js";
@@ -23,7 +23,7 @@ export function useCatalogSkills(operations, sourceId, all, env, reloadKey = 0)
23
23
  : [sourceId];
24
24
  const labels = new Map(sources.map((source) => [source.id, sourceLabel(source)]));
25
25
  const skills = [];
26
- const bundles = [];
26
+ let bundles = [];
27
27
  const discovered = [];
28
28
  const warnings = [];
29
29
  try {
@@ -62,7 +62,12 @@ export function useCatalogSkills(operations, sourceId, all, env, reloadKey = 0)
62
62
  // Ownership is selection- or host-gated, so a loose Skill stays normal until its Package is active.
63
63
  let activeBundles = [];
64
64
  try {
65
- activeBundles = (await packages.activeBundles(discovered)).map(bundle => bundle.descriptor);
65
+ const active = await packages.activeBundles(discovered);
66
+ const installedKeys = new Set(active
67
+ .filter(bundle => bundle.reason === "installed" || bundle.reason === "selected-and-installed")
68
+ .map(bundle => selectionKey(bundle.selection)));
69
+ bundles = bundles.filter(entry => !installedKeys.has(selectionKey(entry.selection)));
70
+ activeBundles = active.map(bundle => bundle.descriptor);
66
71
  }
67
72
  catch (error) {
68
73
  const detail = error instanceof Error ? error.message : String(error);
@@ -7,7 +7,7 @@ const DESCRIPTION_LIMIT = 60;
7
7
  export const UNMANAGED_MARKER = "on disk, unmanaged · adopt it from Installations";
8
8
  export const skillKey = (skill) => `skill:${skill.sourceId}:${skill.path}`;
9
9
  export const bundleKey = (entry) => `bundle:${entry.sourceId}:${entry.descriptor.host}:${bundleRoot(entry.descriptor.coordinates)}`;
10
- function truncate(text) {
10
+ export function truncateDescription(text) {
11
11
  return text.length > DESCRIPTION_LIMIT ? `${text.slice(0, DESCRIPTION_LIMIT - 3)}...` : text;
12
12
  }
13
13
  /** A repository name, with directory and ref for scoped GitHub Sources. */
@@ -69,9 +69,13 @@ export function describeRow(row) {
69
69
  if (data.kind === "bundle") {
70
70
  return `Package ${data.entry.descriptor.name} (${data.entry.descriptor.host}) ${packageVersionLabel(data.entry.descriptor.version)}`;
71
71
  }
72
- return `${data.skill.name} ${truncate(data.skill.description)}`;
72
+ return `${data.skill.name} ${truncateDescription(data.skill.description)}`;
73
73
  }
74
74
  /** The Skills under the tree's Sources, in display order. */
75
75
  export function leavesOf(roots) {
76
76
  return roots.flatMap((root) => (root.children ?? []).flatMap((child) => child.data.kind === "skill" ? [child.data.skill] : []));
77
77
  }
78
+ /** Skills that can still be installed as loose Skills, excluding active Package-owned rows. */
79
+ export function looseLeavesOf(roots) {
80
+ return roots.flatMap((root) => (root.children ?? []).flatMap((child) => child.data.kind === "skill" && child.data.ownedBy === undefined ? [child.data.skill] : []));
81
+ }
@@ -1,12 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Text } from "ink";
2
+ import { Box, Text, useWindowSize } from "ink";
3
3
  import { useEffect, useMemo, useRef, useState } from "react";
4
4
  import { BROWSE } from "./catalog-actions.js";
5
5
  import { handleCatalogBrowseKey } from "./catalog-browse.js";
6
6
  import { filterSkills } from "./catalog-filter.js";
7
7
  import { handleCatalogActionKey } from "./catalog-flow.js";
8
8
  import { isInstalled } from "./catalog-installs.js";
9
- import { ActionPanel, CatalogHeader, CatalogRowLine } from "./catalog-panel.js";
9
+ import { ActionPanel, CatalogHeader, CatalogRowLine, CatalogSelectionHelp } from "./catalog-panel.js";
10
+ import { packageOwnedSkillNotice } from "./catalog-ownership.js";
10
11
  import { packageOperationsOf } from "./environment.js";
11
12
  import { handlePackageKey, isPackageMode, startPackageAction } from "./package-actions.js";
12
13
  import { ListFooter } from "./panel-parts.js";
@@ -18,8 +19,26 @@ import { useMarks, useMounted } from "./view-state.js";
18
19
  import { computeWindow, useListHeight } from "./window.js";
19
20
  import { useKeys } from "./keys.js";
20
21
  const NO_ENVIRONMENT = {};
22
+ function wrappedLineCount(text, columns) {
23
+ let lines = 1;
24
+ let used = 0;
25
+ for (const word of text.split(" ")) {
26
+ const width = [...word].reduce((total, character) => total + ((character.codePointAt(0) ?? 0) > 0xff ? 2 : 1), 0);
27
+ if (used > 0 && used + 1 + width <= columns) {
28
+ used += 1 + width;
29
+ continue;
30
+ }
31
+ if (used > 0) {
32
+ lines += 1;
33
+ used = 0;
34
+ }
35
+ lines += Math.floor((width - 1) / columns);
36
+ used = ((width - 1) % columns) + 1;
37
+ }
38
+ return lines;
39
+ }
21
40
  export function CatalogView({ operations, sourceId, onCapturingChange, environment, listHeight }) {
22
- const height = useListHeight(listHeight, 6);
41
+ const { columns } = useWindowSize();
23
42
  const env = environment ?? NO_ENVIRONMENT;
24
43
  const [all, setAll] = useState(sourceId === undefined);
25
44
  const mounted = useMounted();
@@ -48,6 +67,10 @@ export function CatalogView({ operations, sourceId, onCapturingChange, environme
48
67
  const { restart } = navigation;
49
68
  useEffect(restart, [operations, sourceId, all, restart]);
50
69
  const { rows, index } = navigation;
70
+ const helpLines = state.status === "ready"
71
+ ? Math.max(1, ...state.activeBundles.map((owner) => wrappedLineCount(packageOwnedSkillNotice(owner), Math.max(columns, 1))))
72
+ : 1;
73
+ const height = useListHeight(listHeight, 6 + helpLines);
51
74
  const flow = {
52
75
  operations,
53
76
  env,
@@ -108,5 +131,5 @@ export function CatalogView({ operations, sourceId, onCapturingChange, environme
108
131
  return _jsxs(Text, { color: theme.error, children: ["Error: ", state.message] });
109
132
  const window = computeWindow(rows.length, index, height);
110
133
  const selectedCount = leavesOf(roots).filter((skill) => marks.marked.has(skillKey(skill))).length;
111
- return (_jsxs(Box, { flexDirection: "column", children: [state.warnings.map((warning) => _jsx(Text, { color: theme.warning, children: warning }, warning)), _jsx(CatalogHeader, { scope: scope, listed: visible.length, installable: installable.length, total: skills.length, filter: filter.filter }), rows.slice(window.start, window.end).map((row, offset) => (_jsx(CatalogRowLine, { row: row, selected: window.start + offset === index, marked: marks.marked.has(row.node.id), installed: installed }, row.node.id))), _jsx(ListFooter, { indicator: window.indicator, selectedCount: selectedCount, message: message }), _jsx(ActionPanel, { mode: action })] }));
134
+ return (_jsxs(Box, { flexDirection: "column", children: [state.warnings.map((warning) => _jsx(Text, { color: theme.warning, children: warning }, warning)), _jsx(CatalogHeader, { scope: scope, listed: visible.length, installable: installable.length, total: skills.length, filter: filter.filter }), rows.slice(window.start, window.end).map((row, offset) => (_jsx(CatalogRowLine, { row: row, selected: window.start + offset === index, marked: marks.marked.has(row.node.id), installed: installed }, row.node.id))), _jsx(CatalogSelectionHelp, { row: rows[index], height: helpLines }), _jsx(ListFooter, { indicator: window.indicator, selectedCount: selectedCount, message: message }), _jsx(ActionPanel, { mode: action })] }));
112
135
  }
@@ -10,8 +10,14 @@ export function packageVersionLabel(evidence) {
10
10
  export function installedEvidenceLabel(evidence) {
11
11
  switch (evidence.kind) {
12
12
  case "installed": {
13
- if (evidence.version !== undefined)
14
- return `installed ${evidence.version}`;
13
+ if (evidence.version !== undefined) {
14
+ const qualifier = evidence.reason === undefined ? "" : " (HEAD; local changes)";
15
+ return `installed ${evidence.version}${qualifier}`;
16
+ }
17
+ if (evidence.reason !== undefined) {
18
+ const reason = evidence.reason.includes("local modifications") ? "local changes" : evidence.reason;
19
+ return `installed (version unavailable: ${reason})`;
20
+ }
15
21
  if (evidence.commit !== undefined)
16
22
  return `installed at ${evidence.commit.slice(0, 7)}`;
17
23
  return "installed with no version evidence";
@@ -46,7 +52,7 @@ export function packageRow(record, inspection) {
46
52
  name: inspection.descriptor?.name ?? describeCoordinates(record.selection),
47
53
  installed: installedEvidenceLabel(inspection.installed),
48
54
  available: packageVersionLabel(inspection.available),
49
- status: classifyUpdate(inspection.installed, inspection.available),
55
+ status: classifyUpdate(inspection.installed, inspection.available, inspection.availableCommit, inspection.installedCommitIsAncestor),
50
56
  ...(inspection.drift === undefined ? {} : { drift: inspection.drift }),
51
57
  };
52
58
  }
@@ -3,6 +3,6 @@ import { render } from "ink";
3
3
  import { App } from "./app.js";
4
4
  /** Renders the TUI and resolves once the user exits it. */
5
5
  export async function renderTui(operations, environment) {
6
- const instance = render(_jsx(App, { operations: operations, environment: environment }));
6
+ const instance = render(_jsx(App, { operations: operations, environment: environment }), { alternateScreen: true });
7
7
  await instance.waitUntilExit();
8
8
  }
@@ -1,4 +1,4 @@
1
- import { useStdout } from "ink";
1
+ import { useWindowSize } from "ink";
2
2
  /** Rows assumed when the terminal size is unknown (for example under test renderers). */
3
3
  export const FALLBACK_TERMINAL_ROWS = 24;
4
4
  /** Lines the shell itself uses: title, tab bar, separator and footer hint. */
@@ -23,9 +23,8 @@ export function pageStep(height) {
23
23
  * view's own `reserved` lines. `override` fixes the height for tests.
24
24
  */
25
25
  export function useListHeight(override, reserved) {
26
- const { stdout } = useStdout();
26
+ const { rows } = useWindowSize();
27
27
  if (override !== undefined)
28
28
  return Math.max(override, 1);
29
- const rows = stdout.rows ?? FALLBACK_TERMINAL_ROWS;
30
- return Math.max(rows - APP_CHROME_ROWS - reserved, MINIMUM_ROWS);
29
+ return Math.max((rows || FALLBACK_TERMINAL_ROWS) - APP_CHROME_ROWS - reserved, MINIMUM_ROWS);
31
30
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iseoane/agent-depot",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "A cross-platform catalog for reusable coding-agent skills.",
5
5
  "license": "MIT",
6
6
  "author": "iseoane",
@@ -26,6 +26,7 @@
26
26
  "files": [
27
27
  "dist/src",
28
28
  "!dist/src/**/*.map",
29
+ "resources/pi",
29
30
  "skills",
30
31
  "!skills/**/test_*.py",
31
32
  "!skills/**/testdata",
@@ -41,10 +42,39 @@
41
42
  "engines": {
42
43
  "node": ">=24"
43
44
  },
45
+ "peerDependencies": {
46
+ "@earendil-works/pi-tui": "^1.0.3",
47
+ "typebox": "^1.3.27"
48
+ },
49
+ "peerDependenciesMeta": {
50
+ "@earendil-works/pi-tui": {
51
+ "optional": true
52
+ },
53
+ "typebox": {
54
+ "optional": true
55
+ }
56
+ },
57
+ "devDependencies": {
58
+ "@earendil-works/pi-tui": "1.0.3",
59
+ "@eslint/js": "^9.39.2",
60
+ "@homebridge/node-pty-prebuilt-multiarch": "^0.14.1",
61
+ "@types/node": "^24.19.0",
62
+ "@types/react": "^19.3.0",
63
+ "eslint": "^9.39.2",
64
+ "ink-testing-library": "^4.0.0",
65
+ "typebox": "1.3.27",
66
+ "typescript": "^5.7.2",
67
+ "typescript-eslint": "^8.70.1"
68
+ },
69
+ "pi": {
70
+ "extensions": [
71
+ "./resources/pi/agents-panel/index.ts"
72
+ ]
73
+ },
44
74
  "scripts": {
45
75
  "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
46
76
  "typecheck": "tsc --noEmit",
47
- "lint": "eslint \"src/**/*.{ts,tsx}\" \"tests/**/*.{ts,tsx}\"",
77
+ "lint": "eslint \"src/**/*.{ts,tsx}\" \"resources/pi/**/*.{ts,tsx}\" \"tests/**/*.{ts,tsx}\"",
48
78
  "test": "pnpm build && node --test --import ./dist/tests/isolated-env.js \"dist/tests/*.test.js\"",
49
79
  "test:single": "pnpm build && node -e \"const { spawnSync } = require('node:child_process'); const input = process.argv[1] || 'tests/version.test.ts'; const file = input.replace(/^tests[\\\\/]/, 'dist/tests/').replace(/\\\\.ts$/i, '.js'); process.exit(spawnSync(process.execPath, ['--import', './dist/tests/isolated-env.js', '--test', file], { stdio: 'inherit' }).status ?? 1);\"",
50
80
  "test:coverage": "node -e \"require('node:fs').rmSync('coverage',{recursive:true,force:true})\" && tsc -p tsconfig.coverage.json && node -e \"const { spawnSync } = require('node:child_process'); process.exit(spawnSync(process.execPath, ['--enable-source-maps','--import','./dist/tests/isolated-env.js','--test','--test-reporter=dot','dist/tests/*.test.js'], { stdio: 'inherit', env: { ...process.env, NODE_V8_COVERAGE: 'coverage/v8' } }).status ?? 1);\"",
@@ -56,16 +86,6 @@
56
86
  "prepublishOnly": "pnpm build && pnpm test",
57
87
  "test:pack": "pnpm build && node scripts/pack-smoke.mjs"
58
88
  },
59
- "devDependencies": {
60
- "@eslint/js": "^9.39.2",
61
- "@homebridge/node-pty-prebuilt-multiarch": "^0.14.1",
62
- "@types/node": "^24.19.0",
63
- "@types/react": "^19.3.0",
64
- "eslint": "^9.39.2",
65
- "ink-testing-library": "^4.0.0",
66
- "typescript": "^5.7.2",
67
- "typescript-eslint": "^8.70.1"
68
- },
69
89
  "dependencies": {
70
90
  "ink": "^7.1.1",
71
91
  "react": "^19.3.0"
@@ -0,0 +1,62 @@
1
+ import { Type, type Static } from "typebox";
2
+ import { Value } from "typebox/value";
3
+
4
+ const agentActivitySchema = Type.Object({
5
+ label: Type.String(),
6
+ at: Type.Number(),
7
+ });
8
+
9
+ const agentUsageSchema = Type.Object({
10
+ input: Type.Optional(Type.Number()),
11
+ output: Type.Optional(Type.Number()),
12
+ cacheRead: Type.Optional(Type.Number()),
13
+ cacheWrite: Type.Optional(Type.Number()),
14
+ totalTokens: Type.Optional(Type.Number()),
15
+ cost: Type.Optional(Type.Number()),
16
+ });
17
+
18
+ const agentSchema = Type.Object({
19
+ id: Type.String(),
20
+ description: Type.String(),
21
+ subagentType: Type.String(),
22
+ model: Type.String(),
23
+ thinking: Type.Optional(Type.String()),
24
+ readonly: Type.Boolean(),
25
+ status: Type.Union([
26
+ Type.Literal("running"),
27
+ Type.Literal("completed"),
28
+ Type.Literal("failed"),
29
+ Type.Literal("stopped"),
30
+ ]),
31
+ kind: Type.Union([Type.Literal("local"), Type.Literal("remote"), Type.Literal("ended")]),
32
+ pid: Type.Optional(Type.Number()),
33
+ startedAt: Type.String(),
34
+ endedAt: Type.Optional(Type.String()),
35
+ elapsedMs: Type.Number(),
36
+ observed: Type.Boolean(),
37
+ activity: Type.Optional(agentActivitySchema),
38
+ usage: Type.Optional(agentUsageSchema),
39
+ worktree: Type.Optional(Type.String()),
40
+ });
41
+
42
+ export const agentSnapshotRequestSchema = Type.Object({
43
+ version: Type.Literal(1),
44
+ requestId: Type.String(),
45
+ });
46
+
47
+ export const agentSnapshotResponseSchema = Type.Object({
48
+ version: Type.Literal(1),
49
+ requestId: Type.String(),
50
+ agents: Type.Array(agentSchema),
51
+ });
52
+
53
+ export type AgentSnapshot = Static<typeof agentSchema>;
54
+ export type AgentSnapshotRequest = Static<typeof agentSnapshotRequestSchema>;
55
+ export type AgentSnapshotResponse = Static<typeof agentSnapshotResponseSchema>;
56
+
57
+ export const AGENT_SNAPSHOT_REQUEST_EVENT = "agent-depot:agents:v1:request";
58
+ export const AGENT_SNAPSHOT_RESPONSE_EVENT = "agent-depot:agents:v1:snapshot";
59
+
60
+ export function parseAgentSnapshotResponse(value: unknown): AgentSnapshotResponse | undefined {
61
+ return Value.Check(agentSnapshotResponseSchema, value) ? value : undefined;
62
+ }