jfl 0.9.0 → 0.9.1

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.
Files changed (87) hide show
  1. package/README.md +35 -4
  2. package/dist/commands/digest.d.ts +6 -0
  3. package/dist/commands/digest.d.ts.map +1 -1
  4. package/dist/commands/digest.js +70 -69
  5. package/dist/commands/digest.js.map +1 -1
  6. package/dist/commands/eval.d.ts +40 -0
  7. package/dist/commands/eval.d.ts.map +1 -1
  8. package/dist/commands/eval.js +8 -8
  9. package/dist/commands/eval.js.map +1 -1
  10. package/dist/commands/findings.d.ts +7 -0
  11. package/dist/commands/findings.d.ts.map +1 -1
  12. package/dist/commands/findings.js +4 -4
  13. package/dist/commands/findings.js.map +1 -1
  14. package/dist/commands/ide.js +3 -2
  15. package/dist/commands/ide.js.map +1 -1
  16. package/dist/commands/kanban.js +6 -6
  17. package/dist/commands/kanban.js.map +1 -1
  18. package/dist/commands/portfolio.d.ts +5 -0
  19. package/dist/commands/portfolio.d.ts.map +1 -1
  20. package/dist/commands/portfolio.js +193 -203
  21. package/dist/commands/portfolio.js.map +1 -1
  22. package/dist/commands/predict.d.ts +19 -0
  23. package/dist/commands/predict.d.ts.map +1 -1
  24. package/dist/commands/predict.js +4 -4
  25. package/dist/commands/predict.js.map +1 -1
  26. package/dist/commands/setup.d.ts.map +1 -1
  27. package/dist/commands/setup.js +106 -7
  28. package/dist/commands/setup.js.map +1 -1
  29. package/dist/commands/start.d.ts +25 -0
  30. package/dist/commands/start.d.ts.map +1 -0
  31. package/dist/commands/start.js +191 -0
  32. package/dist/commands/start.js.map +1 -0
  33. package/dist/commands/tenet-agents.js +2 -2
  34. package/dist/commands/tenet-agents.js.map +1 -1
  35. package/dist/commands/tenet-setup.d.ts +2 -1
  36. package/dist/commands/tenet-setup.d.ts.map +1 -1
  37. package/dist/commands/tenet-setup.js +22 -18
  38. package/dist/commands/tenet-setup.js.map +1 -1
  39. package/dist/commands/viz.d.ts +33 -0
  40. package/dist/commands/viz.d.ts.map +1 -1
  41. package/dist/commands/viz.js +9 -9
  42. package/dist/commands/viz.js.map +1 -1
  43. package/dist/index.js +94 -43
  44. package/dist/index.js.map +1 -1
  45. package/dist/lib/advanced-setup.d.ts +1 -1
  46. package/dist/lib/advanced-setup.d.ts.map +1 -1
  47. package/dist/lib/advanced-setup.js +15 -15
  48. package/dist/lib/advanced-setup.js.map +1 -1
  49. package/dist/lib/discovery-agent.js +3 -3
  50. package/dist/lib/discovery-agent.js.map +1 -1
  51. package/dist/lib/linear-id-map.d.ts.map +1 -1
  52. package/dist/lib/linear-id-map.js +2 -0
  53. package/dist/lib/linear-id-map.js.map +1 -1
  54. package/dist/lib/onboarding.d.ts +1 -1
  55. package/dist/lib/onboarding.js +13 -13
  56. package/dist/lib/onboarding.js.map +1 -1
  57. package/dist/lib/rl-manager.d.ts.map +1 -1
  58. package/dist/lib/rl-manager.js +2 -1
  59. package/dist/lib/rl-manager.js.map +1 -1
  60. package/dist/lib/setup/starter-intelligence.d.ts +25 -0
  61. package/dist/lib/setup/starter-intelligence.d.ts.map +1 -0
  62. package/dist/lib/setup/starter-intelligence.js +309 -0
  63. package/dist/lib/setup/starter-intelligence.js.map +1 -0
  64. package/dist/lib/workspace/surface-registry.d.ts.map +1 -1
  65. package/dist/lib/workspace/surface-registry.js +5 -0
  66. package/dist/lib/workspace/surface-registry.js.map +1 -1
  67. package/dist/lib/workspace/surfaces/sidebar.d.ts.map +1 -1
  68. package/dist/lib/workspace/surfaces/sidebar.js +5 -1
  69. package/dist/lib/workspace/surfaces/sidebar.js.map +1 -1
  70. package/dist/lib/workspace/tmux-adapter.d.ts +8 -5
  71. package/dist/lib/workspace/tmux-adapter.d.ts.map +1 -1
  72. package/dist/lib/workspace/tmux-adapter.js +38 -7
  73. package/dist/lib/workspace/tmux-adapter.js.map +1 -1
  74. package/dist/lib/workspace/tmux-sidebar.d.ts +14 -0
  75. package/dist/lib/workspace/tmux-sidebar.d.ts.map +1 -0
  76. package/dist/lib/workspace/tmux-sidebar.js +230 -0
  77. package/dist/lib/workspace/tmux-sidebar.js.map +1 -0
  78. package/dist/utils/jfl-config.d.ts +7 -2
  79. package/dist/utils/jfl-config.d.ts.map +1 -1
  80. package/dist/utils/jfl-config.js +14 -4
  81. package/dist/utils/jfl-config.js.map +1 -1
  82. package/package.json +1 -1
  83. package/packages/pi/extensions/context.ts +51 -1
  84. package/packages/pi/extensions/hub-tools.ts +247 -0
  85. package/packages/pi/extensions/index.ts +4 -0
  86. package/packages/pi/extensions/memory-tool.ts +84 -4
  87. package/packages/pi/extensions/service-skills.ts +214 -0
@@ -1,11 +1,10 @@
1
1
  /**
2
2
  * Memory Tool Extension
3
3
  *
4
- * Registers jfl_memory_search tool with custom TUI rendering.
5
- * Queries Context Hub memory API and renders results with
6
- * type-colored headers and collapsible sections.
4
+ * Registers jfl_memory_search, jfl_memory_add, and jfl_memory_status tools.
5
+ * Full parity with the Context Hub MCP server's memory tools.
7
6
  *
8
- * @purpose jfl_memory_search toolthemed semantic memory search results
7
+ * @purpose Memory toolssearch, add, and status for project memory
9
8
  */
10
9
 
11
10
  import type { PiContext } from "./types.js"
@@ -13,6 +12,7 @@ import { hubUrl, authToken } from "./map-bridge.js"
13
12
  import { memoryRenderCall, memoryRenderResult } from "./tool-renderers.js"
14
13
 
15
14
  export function setupMemoryTool(ctx: PiContext): void {
15
+ // ─── jfl_memory_search ───────────────────────────────────────────────────
16
16
  ctx.registerTool({
17
17
  name: "jfl_memory_search",
18
18
  description: "Search JFL project memory — find past decisions, learnings, and patterns across all sessions",
@@ -70,4 +70,84 @@ export function setupMemoryTool(ctx: PiContext): void {
70
70
  renderCall: memoryRenderCall,
71
71
  renderResult: memoryRenderResult,
72
72
  })
73
+
74
+ // ─── jfl_memory_add ──────────────────────────────────────────────────────
75
+ ctx.registerTool({
76
+ name: "jfl_memory_add",
77
+ description: "Manually add a memory or note to the project memory system. Use to capture insights, decisions, or important context that should persist across sessions.",
78
+ promptSnippet: "Add a memory/note to project memory",
79
+ inputSchema: {
80
+ type: "object",
81
+ properties: {
82
+ title: {
83
+ type: "string",
84
+ description: "Title for the memory entry",
85
+ },
86
+ content: {
87
+ type: "string",
88
+ description: "Content of the memory — the insight, decision, or note",
89
+ },
90
+ type: {
91
+ type: "string",
92
+ description: "Type of memory entry",
93
+ enum: ["decision", "discovery", "insight", "note"],
94
+ },
95
+ },
96
+ required: ["title", "content"],
97
+ },
98
+ async handler(input) {
99
+ const { title, content, type } = input as { title: string; content: string; type?: string }
100
+
101
+ try {
102
+ const resp = await fetch(`${hubUrl}/api/memory/add`, {
103
+ method: "POST",
104
+ headers: {
105
+ "Content-Type": "application/json",
106
+ ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
107
+ },
108
+ body: JSON.stringify({ title, content, type: type ?? "note" }),
109
+ signal: AbortSignal.timeout(5000),
110
+ })
111
+
112
+ if (!resp.ok) return "Failed to add memory — hub returned error."
113
+ const data = await resp.json() as { ok?: boolean; id?: string }
114
+ return data.ok
115
+ ? `Memory added: "${title}" (${type ?? "note"})`
116
+ : "Memory add returned unexpected response."
117
+ } catch {
118
+ return "Memory add unavailable — Context Hub may not be running."
119
+ }
120
+ },
121
+ })
122
+
123
+ // ─── jfl_memory_status ───────────────────────────────────────────────────
124
+ ctx.registerTool({
125
+ name: "jfl_memory_status",
126
+ description: "Get memory system statistics and health — total entries, indexed count, embedding status.",
127
+ promptSnippet: "Check memory system health and stats",
128
+ inputSchema: {
129
+ type: "object",
130
+ properties: {},
131
+ },
132
+ async handler() {
133
+ try {
134
+ const resp = await fetch(`${hubUrl}/api/memory/status`, {
135
+ method: "GET",
136
+ headers: {
137
+ ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
138
+ },
139
+ signal: AbortSignal.timeout(5000),
140
+ })
141
+
142
+ if (!resp.ok) return "Memory status unavailable."
143
+ const data = await resp.json() as Record<string, unknown>
144
+
145
+ return Object.entries(data)
146
+ .map(([k, v]) => `${k}: ${typeof v === "object" ? JSON.stringify(v) : v}`)
147
+ .join("\n")
148
+ } catch {
149
+ return "Memory status unavailable — Context Hub may not be running."
150
+ }
151
+ },
152
+ })
73
153
  }
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Service Skills Bridge
3
+ *
4
+ * Scans .claude/skills/ for service-type skills (generated during jfl init
5
+ * service onboarding) and registers them as Pi commands + tools.
6
+ * This gives Pi parity with Claude Code's service agent access.
7
+ *
8
+ * Service skills in .claude/skills/ have frontmatter `type: service` and
9
+ * include commands like status, logs, start, stop, restart, health.
10
+ *
11
+ * @purpose Bridge .claude/skills/ service agents into Pi sessions
12
+ */
13
+
14
+ import { existsSync, readdirSync, readFileSync } from "fs"
15
+ import { join } from "path"
16
+ import { execSync } from "child_process"
17
+ import type { PiContext, JflConfig } from "./types.js"
18
+
19
+ interface ServiceSkill {
20
+ name: string
21
+ description: string
22
+ servicePath: string
23
+ commands: string[]
24
+ }
25
+
26
+ function parseFrontmatter(content: string): Record<string, string> {
27
+ const match = content.match(/^---\n([\s\S]*?)\n---/)
28
+ if (!match) return {}
29
+ const fm: Record<string, string> = {}
30
+ for (const line of match[1].split("\n")) {
31
+ const [key, ...rest] = line.split(":")
32
+ if (key && rest.length) {
33
+ fm[key.trim()] = rest.join(":").trim()
34
+ }
35
+ }
36
+ return fm
37
+ }
38
+
39
+ function discoverServiceSkills(projectRoot: string): ServiceSkill[] {
40
+ const claudeSkillsDir = join(projectRoot, ".claude", "skills")
41
+ if (!existsSync(claudeSkillsDir)) return []
42
+
43
+ const skills: ServiceSkill[] = []
44
+
45
+ try {
46
+ const entries = readdirSync(claudeSkillsDir, { withFileTypes: true })
47
+
48
+ for (const entry of entries) {
49
+ if (!entry.isDirectory()) continue
50
+
51
+ const skillPath = join(claudeSkillsDir, entry.name, "SKILL.md")
52
+ if (!existsSync(skillPath)) continue
53
+
54
+ try {
55
+ const content = readFileSync(skillPath, "utf-8")
56
+ const fm = parseFrontmatter(content)
57
+
58
+ if (fm.type !== "service") continue
59
+
60
+ // Extract available commands from the markdown table
61
+ const commands: string[] = []
62
+ const cmdMatch = content.match(/\| `(\w+)` \|/g)
63
+ if (cmdMatch) {
64
+ for (const m of cmdMatch) {
65
+ const cmd = m.match(/`(\w+)`/)
66
+ if (cmd) commands.push(cmd[1])
67
+ }
68
+ }
69
+ // Fallback defaults
70
+ if (commands.length === 0) {
71
+ commands.push("status", "logs", "start", "stop", "restart", "health", "recent")
72
+ }
73
+
74
+ skills.push({
75
+ name: entry.name,
76
+ description: fm.description || `${entry.name} service agent`,
77
+ servicePath: fm.service_path || "",
78
+ commands,
79
+ })
80
+ } catch {
81
+ // Skip unparseable skills
82
+ }
83
+ }
84
+ } catch {
85
+ // .claude/skills not readable
86
+ }
87
+
88
+ return skills
89
+ }
90
+
91
+ function runServiceCommand(serviceName: string, command: string, servicePath: string, projectRoot: string): string {
92
+ // Try jfl service-agent first
93
+ try {
94
+ const result = execSync(
95
+ `jfl service-agent run ${serviceName} ${command}`,
96
+ { cwd: projectRoot, encoding: "utf-8", timeout: 15000, stdio: ["pipe", "pipe", "pipe"] }
97
+ )
98
+ return result.trim()
99
+ } catch {
100
+ // Fallback: direct execution based on command type
101
+ }
102
+
103
+ if (!servicePath || !existsSync(servicePath)) {
104
+ return `Service path not found: ${servicePath}`
105
+ }
106
+
107
+ try {
108
+ switch (command) {
109
+ case "status": {
110
+ const branch = execSync("git branch --show-current", { cwd: servicePath, encoding: "utf-8" }).trim()
111
+ const lastCommit = execSync("git log -1 --pretty='%h %s (%cr)'", { cwd: servicePath, encoding: "utf-8" }).trim()
112
+ const dirty = execSync("git status --porcelain", { cwd: servicePath, encoding: "utf-8" }).trim()
113
+ return [
114
+ `Service: ${serviceName}`,
115
+ `Path: ${servicePath}`,
116
+ `Branch: ${branch}`,
117
+ `Last commit: ${lastCommit}`,
118
+ `Working tree: ${dirty ? `${dirty.split("\n").length} changes` : "clean"}`,
119
+ ].join("\n")
120
+ }
121
+
122
+ case "logs": {
123
+ return execSync("git log --oneline -10", { cwd: servicePath, encoding: "utf-8" }).trim()
124
+ }
125
+
126
+ case "recent": {
127
+ const since = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString()
128
+ try {
129
+ return execSync(`git log --oneline --since="${since}"`, { cwd: servicePath, encoding: "utf-8" }).trim() || "No changes in last 24h"
130
+ } catch {
131
+ return "No changes in last 24h"
132
+ }
133
+ }
134
+
135
+ case "health": {
136
+ // Check if package.json exists and has a test script
137
+ const pkgPath = join(servicePath, "package.json")
138
+ if (existsSync(pkgPath)) {
139
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"))
140
+ const hasTests = !!pkg.scripts?.test
141
+ const hasBuild = !!pkg.scripts?.build
142
+ return [
143
+ `Service: ${serviceName}`,
144
+ `Has tests: ${hasTests ? "yes" : "no"}`,
145
+ `Has build: ${hasBuild ? "yes" : "no"}`,
146
+ `Dependencies: ${Object.keys(pkg.dependencies || {}).length}`,
147
+ `Dev dependencies: ${Object.keys(pkg.devDependencies || {}).length}`,
148
+ ].join("\n")
149
+ }
150
+ return `No package.json found at ${servicePath}`
151
+ }
152
+
153
+ default:
154
+ return `Unknown command: ${command}. Available: status, logs, recent, health`
155
+ }
156
+ } catch (err) {
157
+ return `Error running ${command} on ${serviceName}: ${err}`
158
+ }
159
+ }
160
+
161
+ export async function setupServiceSkills(ctx: PiContext, _config: JflConfig): Promise<void> {
162
+ const root = ctx.session.projectRoot
163
+ const services = discoverServiceSkills(root)
164
+
165
+ if (services.length === 0) return
166
+
167
+ ctx.log(`Discovered ${services.length} service skill(s): ${services.map(s => s.name).join(", ")}`, "debug")
168
+
169
+ // Register a unified service tool
170
+ ctx.registerTool({
171
+ name: "jfl_service",
172
+ description: `Query registered service agents. Available services: ${services.map(s => s.name).join(", ")}. Commands: status, logs, recent, health, start, stop, restart.`,
173
+ promptSnippet: `Service agents: ${services.map(s => s.name).join(", ")}`,
174
+ promptGuidelines: [
175
+ `Use this tool to interact with registered services: ${services.map(s => s.name).join(", ")}`,
176
+ "Common commands: status (git info), logs (recent commits), recent (24h changes), health (package info)",
177
+ ],
178
+ inputSchema: {
179
+ type: "object",
180
+ properties: {
181
+ service: {
182
+ type: "string",
183
+ description: `Service name: ${services.map(s => s.name).join(", ")}`,
184
+ enum: services.map(s => s.name),
185
+ },
186
+ command: {
187
+ type: "string",
188
+ description: "Command to run on the service",
189
+ enum: ["status", "logs", "recent", "health", "start", "stop", "restart"],
190
+ },
191
+ },
192
+ required: ["service", "command"],
193
+ },
194
+ async handler(input) {
195
+ const { service, command } = input as { service: string; command: string }
196
+ const svc = services.find(s => s.name === service)
197
+ if (!svc) return `Unknown service: ${service}. Available: ${services.map(s => s.name).join(", ")}`
198
+ return runServiceCommand(service, command, svc.servicePath, root)
199
+ },
200
+ })
201
+
202
+ // Register per-service commands (e.g., /jfl-cli status, /jfl-platform logs)
203
+ for (const svc of services) {
204
+ ctx.registerCommand({
205
+ name: svc.name,
206
+ description: `${svc.description} — commands: ${svc.commands.join(", ")}`,
207
+ async handler(args: string, _ctx: PiContext) {
208
+ const command = args.trim().split(/\s+/)[0] || "status"
209
+ const result = runServiceCommand(svc.name, command, svc.servicePath, root)
210
+ ctx.ui.notify(result, { level: "info" })
211
+ },
212
+ })
213
+ }
214
+ }