pi-honcho-memory 0.2.0 → 0.3.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.
@@ -0,0 +1,33 @@
1
+ # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2
+ # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
3
+
4
+ name: Node.js Package
5
+
6
+ on:
7
+ release:
8
+ types: [created]
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-node@v4
16
+ with:
17
+ node-version: 20
18
+ - run: npm ci
19
+ - run: npm test
20
+
21
+ publish-npm:
22
+ needs: build
23
+ runs-on: ubuntu-latest
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+ - uses: actions/setup-node@v4
27
+ with:
28
+ node-version: 20
29
+ registry-url: https://registry.npmjs.org/
30
+ - run: npm ci
31
+ - run: npm publish
32
+ env:
33
+ NODE_AUTH_TOKEN: ${{secrets.npm_token}}
@@ -1,8 +1,16 @@
1
1
  import { Honcho } from "@honcho-ai/sdk";
2
2
  import type { Peer, Session } from "@honcho-ai/sdk";
3
3
  import type { HonchoConfig } from "./config.js";
4
+ import { readConfigFile } from "./config.js";
4
5
  import { deriveSessionKey } from "./session.js";
5
6
 
7
+ export interface LinkedHostHandle {
8
+ name: string;
9
+ honcho: Honcho;
10
+ userPeer: Peer;
11
+ aiPeer: Peer;
12
+ }
13
+
6
14
  export interface HonchoHandles {
7
15
  honcho: Honcho;
8
16
  userPeer: Peer;
@@ -10,6 +18,7 @@ export interface HonchoHandles {
10
18
  session: Session | null;
11
19
  sessionKey: string;
12
20
  config: HonchoConfig;
21
+ linked: LinkedHostHandle[];
13
22
  }
14
23
 
15
24
  let cachedHandles: HonchoHandles | null = null;
@@ -51,14 +60,41 @@ export const bootstrap = async (config: HonchoConfig, cwd: string): Promise<Honc
51
60
  honcho.peer(config.aiPeer),
52
61
  ]);
53
62
 
63
+ // Bootstrap linked host clients (read-only cross-workspace access)
64
+ const linked: LinkedHostHandle[] = [];
65
+ if (config.linkedHosts.length > 0) {
66
+ const file = await readConfigFile();
67
+ const allHosts = file?.hosts ?? {};
68
+ const settled = await Promise.allSettled(
69
+ config.linkedHosts.map(async (hostName): Promise<LinkedHostHandle | null> => {
70
+ const hc = allHosts[hostName];
71
+ if (!hc?.workspace || !hc?.aiPeer) return null;
72
+ const h = new Honcho({
73
+ apiKey: config.apiKey,
74
+ baseURL: config.baseURL,
75
+ workspaceId: hc.workspace,
76
+ environment: config.environment,
77
+ });
78
+ const [up, ap] = await Promise.all([h.peer(config.peerName), h.peer(hc.aiPeer)]);
79
+ return { name: hostName, honcho: h, userPeer: up, aiPeer: ap };
80
+ }),
81
+ );
82
+ for (const result of settled) {
83
+ if (result.status === "fulfilled" && result.value) linked.push(result.value);
84
+ else if (result.status === "rejected" && config.logging) {
85
+ console.error("[honcho-memory] linked host bootstrap failed:", result.reason);
86
+ }
87
+ }
88
+ }
89
+
54
90
  if (config.recallMode === "tools") {
55
91
  // Lazy: defer session creation until first tool call
56
- cachedHandles = { honcho, userPeer, aiPeer, session: null, sessionKey, config };
92
+ cachedHandles = { honcho, userPeer, aiPeer, session: null, sessionKey, config, linked };
57
93
  return cachedHandles;
58
94
  }
59
95
 
60
96
  // Eager: full init
61
- cachedHandles = { honcho, userPeer, aiPeer, session: null, sessionKey, config };
97
+ cachedHandles = { honcho, userPeer, aiPeer, session: null, sessionKey, config, linked };
62
98
  await ensureSession(cachedHandles);
63
99
  return cachedHandles;
64
100
  };
@@ -52,6 +52,7 @@ export interface HonchoConfigFile {
52
52
  contextRefresh?: { messageThreshold?: number };
53
53
  logging?: boolean;
54
54
  hosts?: {
55
+ [key: string]: Partial<PiHostConfig> | undefined;
55
56
  pi?: Partial<PiHostConfig>;
56
57
  };
57
58
  }
@@ -44,10 +44,10 @@ const ensureHandles = async () => {
44
44
  return handles;
45
45
  };
46
46
 
47
- const formatSearch = (results: Array<{ peerId: string; content: string }>, preview: number): string => {
47
+ const formatSearch = (results: Array<{ sourceHost?: string; peerId?: string; content: string }>, preview: number): string => {
48
48
  if (results.length === 0) return "No relevant memory found.";
49
49
  return results
50
- .map((entry, index) => `${index + 1}. [${entry.peerId}] ${entry.content.slice(0, preview)}`)
50
+ .map((entry, index) => `${index + 1}. [${entry.sourceHost || "pi"} | ${entry.peerId || "unknown"}] ${entry.content.slice(0, preview)}`)
51
51
  .join("\n\n");
52
52
  };
53
53
 
@@ -57,7 +57,7 @@ export const registerTools = (pi: ExtensionAPI): void => {
57
57
  pi.registerTool({
58
58
  name: "honcho_profile",
59
59
  label: "Honcho Profile",
60
- description: "Retrieve what Honcho currently knows about the user profile.",
60
+ description: "Retrieve what Honcho currently knows about the user profile from this and linked workspaces.",
61
61
  parameters: Type.Object({}),
62
62
  async execute(_toolCallId, _params, _signal, _onUpdate, _ctx) {
63
63
  const handles = await ensureHandles();
@@ -67,8 +67,23 @@ export const registerTools = (pi: ExtensionAPI): void => {
67
67
  peerTarget: handles.userPeer,
68
68
  tokens: handles.config.contextTokens,
69
69
  });
70
+ let result = `=== [pi] ===\n${context.peerRepresentation?.trim() || "No profile memory available yet."}`;
71
+
72
+ for (const lh of handles.linked) {
73
+ try {
74
+ const rep = await lh.aiPeer.representation({ target: lh.userPeer });
75
+ if (rep?.trim()) {
76
+ result += `\n\n=== [${lh.name}] ===\n${rep.trim()}`;
77
+ }
78
+ } catch (err) {
79
+ if (handles.config.logging) {
80
+ console.error(`[honcho-memory] ${lh.name} profile read failed:`, err instanceof Error ? err.message : err);
81
+ }
82
+ }
83
+ }
84
+
70
85
  return {
71
- content: [{ type: "text", text: context.peerRepresentation?.trim() || "No profile memory available yet." }],
86
+ content: [{ type: "text", text: result }],
72
87
  details: {},
73
88
  };
74
89
  },
@@ -77,12 +92,33 @@ export const registerTools = (pi: ExtensionAPI): void => {
77
92
  pi.registerTool({
78
93
  name: "honcho_search",
79
94
  label: "Honcho Search",
80
- description: "Search durable memory for prior conversations, facts, and decisions.",
95
+ description: "Search durable memory for prior conversations, facts, and decisions across all linked workspaces.",
81
96
  parameters: Type.Object({ query: Type.String({ description: "Search query" }) }),
82
97
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
83
98
  const handles = await ensureHandles();
84
99
  const limit = Math.min(handles.config.searchLimit, SEARCH_MAX_TOKENS);
85
- const results = await handles.session!.search(params.query, { limit });
100
+ const results: Array<{ sourceHost?: string; peerId?: string; content: string }> = [];
101
+
102
+ try {
103
+ const mainResults = await handles.session!.search(params.query, { limit });
104
+ results.push(...mainResults.map((r: any) => ({ ...r, sourceHost: "pi" })));
105
+ } catch (err) {
106
+ if (handles.config.logging) {
107
+ console.error("[honcho-memory] primary search failed:", err instanceof Error ? err.message : err);
108
+ }
109
+ }
110
+
111
+ for (const lh of handles.linked) {
112
+ try {
113
+ const hostResults = await lh.honcho.search(params.query, { limit });
114
+ results.push(...hostResults.map((r: any) => ({ ...r, sourceHost: lh.name })));
115
+ } catch (err) {
116
+ if (handles.config.logging) {
117
+ console.error(`[honcho-memory] ${lh.name} search failed:`, err instanceof Error ? err.message : err);
118
+ }
119
+ }
120
+ }
121
+
86
122
  return {
87
123
  content: [{ type: "text", text: formatSearch(results, handles.config.toolPreviewLength) }],
88
124
  details: { count: results.length },
@@ -93,7 +129,7 @@ export const registerTools = (pi: ExtensionAPI): void => {
93
129
  pi.registerTool({
94
130
  name: "honcho_context",
95
131
  label: "Honcho Context",
96
- description: "Ask Honcho to synthesize memory context for the current question.",
132
+ description: "Ask Honcho to synthesize memory context for the current question across all linked workspaces.",
97
133
  parameters: Type.Object({
98
134
  query: Type.String({ description: "Question to ask about long-term memory" }),
99
135
  reasoningLevel: Type.Optional(StringEnum(["minimal", "low", "medium", "high", "max"] as const)),
@@ -103,6 +139,7 @@ export const registerTools = (pi: ExtensionAPI): void => {
103
139
  const truncatedQuery = params.query.slice(0, handles.config.dialecticMaxInputChars);
104
140
  const level = params.reasoningLevel
105
141
  ?? dynamicLevel(truncatedQuery, handles.config.reasoningLevel, handles.config.dialecticDynamic, handles.config.reasoningLevelCap);
142
+
106
143
  let reply = await handles.aiPeer.chat(truncatedQuery, {
107
144
  target: handles.userPeer,
108
145
  session: handles.session!,
@@ -118,7 +155,33 @@ export const registerTools = (pi: ExtensionAPI): void => {
118
155
  });
119
156
  }
120
157
  }
121
- const result = reply?.slice(0, handles.config.dialecticMaxChars) ?? "No additional context available.";
158
+ let result = `=== [pi] ===\n${reply?.slice(0, handles.config.dialecticMaxChars) ?? "No additional context available."}`;
159
+
160
+ for (const lh of handles.linked) {
161
+ try {
162
+ let hostReply = await lh.aiPeer.chat(truncatedQuery, {
163
+ target: lh.userPeer,
164
+ reasoningLevel: level,
165
+ });
166
+ if (!hostReply?.trim()) {
167
+ const bumped = nextLevel(level);
168
+ if (bumped) {
169
+ hostReply = await lh.aiPeer.chat(truncatedQuery, {
170
+ target: lh.userPeer,
171
+ reasoningLevel: bumped,
172
+ });
173
+ }
174
+ }
175
+ if (hostReply?.trim()) {
176
+ result += `\n\n=== [${lh.name}] ===\n${hostReply.slice(0, handles.config.dialecticMaxChars)}`;
177
+ }
178
+ } catch (err) {
179
+ if (handles.config.logging) {
180
+ console.error(`[honcho-memory] ${lh.name} context failed:`, err instanceof Error ? err.message : err);
181
+ }
182
+ }
183
+ }
184
+
122
185
  return {
123
186
  content: [{ type: "text", text: result }],
124
187
  details: {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-honcho-memory",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Honcho-backed persistent memory extension for PI coding agent. Full Hermes feature parity.",
5
5
  "type": "module",
6
6
  "license": "MIT",