@roarpeng/graphflow 1.14.0 → 1.15.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.
Files changed (83) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +3 -3
  3. package/README.zh.md +1 -1
  4. package/dist/config/schema.d.ts +3 -1
  5. package/dist/config/schema.d.ts.map +1 -1
  6. package/dist/graph/client-factory.d.ts.map +1 -1
  7. package/dist/graph/client-factory.js +4 -1
  8. package/dist/graph/client-factory.js.map +1 -1
  9. package/dist/graph/graphify-mcp-client.d.ts +33 -0
  10. package/dist/graph/graphify-mcp-client.d.ts.map +1 -1
  11. package/dist/graph/graphify-mcp-client.js +66 -5
  12. package/dist/graph/graphify-mcp-client.js.map +1 -1
  13. package/dist/index.d.ts +3 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +10 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/integrations/dsh-harness-installer.d.ts +2 -0
  18. package/dist/integrations/dsh-harness-installer.d.ts.map +1 -1
  19. package/dist/integrations/dsh-harness-installer.js +9 -3
  20. package/dist/integrations/dsh-harness-installer.js.map +1 -1
  21. package/dist/integrations/host-adapter-install.d.ts +19 -0
  22. package/dist/integrations/host-adapter-install.d.ts.map +1 -0
  23. package/dist/integrations/host-adapter-install.js +75 -0
  24. package/dist/integrations/host-adapter-install.js.map +1 -0
  25. package/dist/integrations/host-adapter.d.ts +5 -0
  26. package/dist/integrations/host-adapter.d.ts.map +1 -1
  27. package/dist/integrations/host-adapter.js.map +1 -1
  28. package/dist/integrations/skill-installer.d.ts.map +1 -1
  29. package/dist/integrations/skill-installer.js +3 -1
  30. package/dist/integrations/skill-installer.js.map +1 -1
  31. package/dist/security/rbac.d.ts +39 -0
  32. package/dist/security/rbac.d.ts.map +1 -0
  33. package/dist/security/rbac.js +213 -0
  34. package/dist/security/rbac.js.map +1 -0
  35. package/dist/security/token-auth.d.ts +15 -2
  36. package/dist/security/token-auth.d.ts.map +1 -1
  37. package/dist/security/token-auth.js +86 -5
  38. package/dist/security/token-auth.js.map +1 -1
  39. package/dist/surfaces/cli/index.js +79 -4
  40. package/dist/surfaces/cli/index.js.map +1 -1
  41. package/dist/surfaces/cli/init.d.ts +3 -0
  42. package/dist/surfaces/cli/init.d.ts.map +1 -1
  43. package/dist/surfaces/cli/init.js +31 -2
  44. package/dist/surfaces/cli/init.js.map +1 -1
  45. package/dist/surfaces/cli/output.d.ts.map +1 -1
  46. package/dist/surfaces/cli/output.js +7 -3
  47. package/dist/surfaces/cli/output.js.map +1 -1
  48. package/dist/surfaces/cli/runtime/graph.d.ts +13 -0
  49. package/dist/surfaces/cli/runtime/graph.d.ts.map +1 -1
  50. package/dist/surfaces/cli/runtime/graph.js +97 -9
  51. package/dist/surfaces/cli/runtime/graph.js.map +1 -1
  52. package/dist/surfaces/cli/runtime/routing.d.ts +1 -1
  53. package/dist/surfaces/cli/runtime/routing.d.ts.map +1 -1
  54. package/dist/surfaces/cli/runtime/routing.js +12 -2
  55. package/dist/surfaces/cli/runtime/routing.js.map +1 -1
  56. package/dist/surfaces/cli/runtime/types.d.ts +3 -0
  57. package/dist/surfaces/cli/runtime/types.d.ts.map +1 -1
  58. package/dist/surfaces/cli/runtime.d.ts +1 -1
  59. package/dist/surfaces/cli/runtime.d.ts.map +1 -1
  60. package/dist/surfaces/cli/runtime.js +3 -2
  61. package/dist/surfaces/cli/runtime.js.map +1 -1
  62. package/dist/surfaces/mcp/server.d.ts +2 -0
  63. package/dist/surfaces/mcp/server.d.ts.map +1 -1
  64. package/dist/surfaces/mcp/server.js +77 -37
  65. package/dist/surfaces/mcp/server.js.map +1 -1
  66. package/dist/surfaces/mcp/tool-handlers.d.ts.map +1 -1
  67. package/dist/surfaces/mcp/tool-handlers.js +2 -0
  68. package/dist/surfaces/mcp/tool-handlers.js.map +1 -1
  69. package/dist/surfaces/team/diagnose.d.ts +18 -0
  70. package/dist/surfaces/team/diagnose.d.ts.map +1 -0
  71. package/dist/surfaces/team/diagnose.js +57 -0
  72. package/dist/surfaces/team/diagnose.js.map +1 -0
  73. package/dist/surfaces/team/ops.d.ts +16 -0
  74. package/dist/surfaces/team/ops.d.ts.map +1 -0
  75. package/dist/surfaces/team/ops.js +137 -0
  76. package/dist/surfaces/team/ops.js.map +1 -0
  77. package/dist/surfaces/team/server.d.ts +48 -0
  78. package/dist/surfaces/team/server.d.ts.map +1 -0
  79. package/dist/surfaces/team/server.js +393 -0
  80. package/dist/surfaces/team/server.js.map +1 -0
  81. package/dsh/plugin.mjs +180 -46
  82. package/package.json +7 -6
  83. package/plugin.json +1 -1
package/dsh/plugin.mjs CHANGED
@@ -55,7 +55,7 @@ const CAPTURE_OUTPUT_MAX_BYTES = 4_000_000;
55
55
 
56
56
  const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
57
57
 
58
- /** @typedef {{ spawn?: typeof spawn, env?: NodeJS.ProcessEnv, cwd?: string, readSkill?: () => { name: string, description: string, content: string, source: string, path?: string } }} GraphFlowDshPluginConfig */
58
+ /** @typedef {{ spawn?: typeof spawn, env?: NodeJS.ProcessEnv, cwd?: string, log?: { warn?: (msg: string) => void, error?: (msg: string) => void }, readSkill?: () => { name: string, description: string, content: string, source: string, path?: string } }} GraphFlowDshPluginConfig */
59
59
 
60
60
  function envOf(config) {
61
61
  return config?.env ?? process.env;
@@ -154,6 +154,21 @@ export function buildContextHint(cwd = process.cwd()) {
154
154
  return `GraphFlow: before large code reads, call mcp__graphflow__graphflow_context with rootDir=${cwd}. Do not dump SKILL.md.`;
155
155
  }
156
156
 
157
+ /**
158
+ * Same-step first-turn hint message. Tagged as plugin instructions so
159
+ * `isUserOriginatedMessage` does not treat it as a user question.
160
+ * `inject()` is the wrong primitive here — it lands in the *next* step inbox.
161
+ * @param {string} [cwd]
162
+ */
163
+ export function buildHintMessage(cwd = process.cwd()) {
164
+ return {
165
+ id: crypto.randomUUID(),
166
+ role: "user",
167
+ content: [{ type: "text", text: buildContextHint(cwd) }],
168
+ source: { kind: "plugin", plugin: PLUGIN_ID, form: "instructions" },
169
+ };
170
+ }
171
+
157
172
  /**
158
173
  * Extract the plain text of a user message (its text ContentBlocks joined).
159
174
  * Returns "" when the message carries no text. Never throws.
@@ -824,23 +839,66 @@ async function rpcNodesHandler(endpoint, payload, signal, config) {
824
839
  }
825
840
  }
826
841
 
842
+ function logGlue(config, level, message) {
843
+ try {
844
+ const logger = config?.log;
845
+ if (logger && typeof logger[level] === "function") {
846
+ logger[level](`[graphflow-dsh] ${message}`);
847
+ return;
848
+ }
849
+ const fallback = level === "error" ? console.error : console.warn;
850
+ fallback(`[graphflow-dsh] ${message}`);
851
+ } catch {
852
+ // logging must never throw into the harness
853
+ }
854
+ }
855
+
827
856
  /**
828
- * Best-effort registration of the /gf Connection RPC channel
829
- * (`ctx.connection.rpc.handle("/gf", handler, { authority: "trusted-host" })`
830
- * see dsh-client-connection lib/index.js:219-258: the handler is
831
- * `(endpoint, payload, signal)` and `handle` returns a disposer). Duck-typed,
832
- * idempotent (one channel per connection instance), and the returned disposer
833
- * both unregisters via the handle disposer and is attached to `ctx.effect`
834
- * when available so the harness tears it down on unload. Never throws.
835
- * @param {object} ctx
836
- * @param {GraphFlowDshPluginConfig} [config]
857
+ * Resolve a Connection service from a Cordis ctx, an inject fork, or the
858
+ * connection object itself. `ctx.get("connection")` may be undefined while
859
+ * `ctx.connection` is already set (or the reverse); try both. Never throws.
860
+ * @param {object|undefined} source
861
+ * @returns {object|undefined}
862
+ */
863
+ export function resolveConnectionService(source) {
864
+ if (!source || typeof source !== "object") return undefined;
865
+ if (typeof source.get === "function") {
866
+ try {
867
+ const viaGet = source.get("connection");
868
+ if (viaGet && typeof viaGet === "object") return viaGet;
869
+ } catch {
870
+ // service not ready / get threw
871
+ }
872
+ }
873
+ if (source.connection && typeof source.connection === "object") return source.connection;
874
+ if (source.rpc && typeof source.rpc.handle === "function") return source;
875
+ return undefined;
876
+ }
877
+
878
+ function attachRpcEffect(targetCtx, dispose) {
879
+ if (!targetCtx || typeof targetCtx.effect !== "function") return;
880
+ try {
881
+ targetCtx.effect(() => dispose, "graphflow-dsh: /gf nodes rpc channel");
882
+ } catch {
883
+ // effect bus missing — the apply return value still cleans up
884
+ }
885
+ }
886
+
887
+ /**
888
+ * Wire `/gf` on one connection instance. Idempotent per connection.
889
+ * Failures are logged (not swallowed). Never throws.
837
890
  * @returns {(() => void)|undefined}
838
891
  */
839
- function registerNodesRpcChannel(ctx, config) {
840
- const connection = typeof ctx.get === "function" ? ctx.get("connection") : ctx.connection;
841
- if (!connection || typeof connection !== "object") return undefined;
892
+ function registerNodesRpcOnConnection(connection, ctx, config) {
893
+ if (!connection || typeof connection !== "object") {
894
+ logGlue(config, "warn", "/gf RPC: connection service unavailable");
895
+ return undefined;
896
+ }
842
897
  const rpc = connection.rpc;
843
- if (!rpc || typeof rpc.handle !== "function") return undefined;
898
+ if (!rpc || typeof rpc.handle !== "function") {
899
+ logGlue(config, "warn", "/gf RPC: connection.rpc.handle is missing");
900
+ return undefined;
901
+ }
844
902
  if (wiredRpcConnections.has(connection)) return undefined;
845
903
  wiredRpcConnections.add(connection);
846
904
  let disposer;
@@ -848,8 +906,13 @@ function registerNodesRpcChannel(ctx, config) {
848
906
  disposer = rpc.handle(NODES_CHANNEL, (endpoint, payload, signal) => rpcNodesHandler(endpoint, payload, signal, config), {
849
907
  authority: "trusted-host",
850
908
  });
851
- } catch {
909
+ } catch (error) {
852
910
  wiredRpcConnections.delete(connection);
911
+ logGlue(
912
+ config,
913
+ "error",
914
+ `/gf RPC: rpc.handle("${NODES_CHANNEL}") failed: ${error && error.message ? error.message : error}`
915
+ );
853
916
  return undefined;
854
917
  }
855
918
  const dispose = () => {
@@ -863,14 +926,83 @@ function registerNodesRpcChannel(ctx, config) {
863
926
  }
864
927
  }
865
928
  };
866
- if (typeof ctx.effect === "function") {
929
+ attachRpcEffect(ctx, dispose);
930
+ return dispose;
931
+ }
932
+
933
+ /**
934
+ * Best-effort registration of the /gf Connection RPC channel
935
+ * (`ctx.connection.rpc.handle("/gf", handler, { authority: "trusted-host" })`
936
+ * — see dsh-client-connection lib/index.js:219-258: the handler is
937
+ * `(endpoint, payload, signal)` and `handle` returns a disposer).
938
+ *
939
+ * The host `connection` service is often not ready at apply() time
940
+ * (`ctx.get("connection")` is undefined because it injects `webRuntime`).
941
+ * Mirror DSH api-gateway: wait with `ctx.inject(["connection"], cb)` and
942
+ * register when the service appears. Duck-typed, idempotent (one channel
943
+ * per connection), and the returned disposer both unregisters via the
944
+ * handle disposer and is attached to `ctx.effect` when available.
945
+ * Registration failures are logged. Never throws.
946
+ * @param {object} ctx
947
+ * @param {GraphFlowDshPluginConfig} [config]
948
+ * @returns {(() => void)|undefined}
949
+ */
950
+ function registerNodesRpcChannel(ctx, config) {
951
+ const disposeHolder = { current: undefined };
952
+ let disposed = false;
953
+
954
+ const tryRegister = (sourceCtx) => {
955
+ if (disposed) return undefined;
956
+ const connection = resolveConnectionService(sourceCtx) ?? resolveConnectionService(ctx);
957
+ if (!connection) return undefined;
958
+ const dispose = registerNodesRpcOnConnection(connection, sourceCtx ?? ctx, config);
959
+ if (dispose) {
960
+ disposeHolder.current = dispose;
961
+ attachRpcEffect(ctx, dispose);
962
+ }
963
+ return dispose;
964
+ };
965
+
966
+ tryRegister(ctx);
967
+
968
+ if (!disposeHolder.current && ctx && typeof ctx.inject === "function") {
969
+ logGlue(config, "warn", '/gf RPC: connection not ready; waiting via ctx.inject(["connection"])');
867
970
  try {
868
- ctx.effect(() => dispose, "graphflow-dsh: /gf nodes rpc channel");
869
- } catch {
870
- // effect bus missing — the apply return value still cleans up
971
+ ctx.inject(["connection"], (injected) => {
972
+ try {
973
+ if (disposed) return;
974
+ const dispose = tryRegister(injected ?? ctx);
975
+ if (!dispose && !disposeHolder.current) {
976
+ logGlue(config, "warn", "/gf RPC: connection inject fired but /gf channel was not registered");
977
+ }
978
+ } catch (error) {
979
+ logGlue(
980
+ config,
981
+ "error",
982
+ `/gf RPC: delayed registration failed: ${error && error.message ? error.message : error}`
983
+ );
984
+ }
985
+ });
986
+ } catch (error) {
987
+ logGlue(
988
+ config,
989
+ "error",
990
+ `/gf RPC: ctx.inject(["connection"]) failed: ${error && error.message ? error.message : error}`
991
+ );
871
992
  }
872
993
  }
873
- return dispose;
994
+
995
+ return () => {
996
+ disposed = true;
997
+ if (typeof disposeHolder.current === "function") {
998
+ try {
999
+ disposeHolder.current();
1000
+ } catch {
1001
+ // teardown is best-effort
1002
+ }
1003
+ disposeHolder.current = undefined;
1004
+ }
1005
+ };
874
1006
  }
875
1007
 
876
1008
  /**
@@ -1067,17 +1199,6 @@ export function closePendingEpisodeForCwd(cwd, config = {}) {
1067
1199
  }
1068
1200
  }
1069
1201
 
1070
- function safeInjectHint(agent, cwd) {
1071
- if (!agent || typeof agent.inject !== "function") return;
1072
- const text = buildContextHint(cwd);
1073
- agent.inject({
1074
- id: crypto.randomUUID(),
1075
- role: "user",
1076
- content: [{ type: "text", text }],
1077
- source: { kind: "plugin", plugin: PLUGIN_ID, form: "instructions" },
1078
- });
1079
- }
1080
-
1081
1202
  function registerSkill(ctx, config) {
1082
1203
  const skills = ctx?.skills;
1083
1204
  if (!skills || typeof skills.register !== "function") return;
@@ -1104,14 +1225,18 @@ export function apply(ctx, config = {}) {
1104
1225
  }
1105
1226
 
1106
1227
  // Static-panel data channel: /gf Connection RPC (registerNodesRpcChannel).
1107
- // Best-effort a missing connection service or a failed registration must
1108
- // never affect the rest of the glue. The disposer is returned from apply so
1109
- // Cordis tears the channel down on unload.
1228
+ // Registers immediately when connection is already on ctx; otherwise waits
1229
+ // via ctx.inject(["connection"]) so apply() racing webRuntime still works.
1230
+ // Failures are logged. Never throw into the harness.
1110
1231
  let disposeNodesRpc;
1111
1232
  try {
1112
1233
  disposeNodesRpc = registerNodesRpcChannel(ctx, config);
1113
- } catch {
1114
- // the /gf data channel is optional
1234
+ } catch (error) {
1235
+ logGlue(
1236
+ config,
1237
+ "error",
1238
+ `/gf RPC: registerNodesRpcChannel threw: ${error && error.message ? error.message : error}`
1239
+ );
1115
1240
  }
1116
1241
 
1117
1242
  const hinted = new WeakSet();
@@ -1259,20 +1384,29 @@ export function apply(ctx, config = {}) {
1259
1384
  }
1260
1385
 
1261
1386
  try {
1262
- listen(ctx, "agent/pre-step", (payload, next) => {
1387
+ // First-turn hint must ride in the *same* step as the user's first
1388
+ // message. `agent.inject()` from pre-step appends to the *next* step
1389
+ // inbox (and can spawn a trailing step the model refuses). Extend the
1390
+ // enter decision's `messages` after `next()` instead — same WeakSet
1391
+ // gating and source tagging, no driver wake.
1392
+ listen(ctx, "agent/pre-step", async (payload, next) => {
1393
+ let decision;
1263
1394
  try {
1264
- const agent = payloadAgent(payload);
1265
- if (agent && !hinted.has(agent)) {
1266
- hinted.add(agent);
1267
- safeInjectHint(agent, resolveWorkspaceCwd(payload, config.cwd));
1268
- }
1395
+ decision = typeof next === "function" ? await next() : undefined;
1269
1396
  } catch {
1270
- // hint is optional
1397
+ return undefined;
1271
1398
  }
1272
- if (typeof next === "function") {
1273
- return next();
1399
+ try {
1400
+ if (!decision || decision.kind !== "enter") return decision;
1401
+ const agent = payloadAgent(payload);
1402
+ if (!agent || hinted.has(agent)) return decision;
1403
+ const hint = buildHintMessage(resolveWorkspaceCwd(payload, config.cwd));
1404
+ const messages = Array.isArray(decision.messages) ? decision.messages : [];
1405
+ hinted.add(agent);
1406
+ return { ...decision, messages: [...messages, hint] };
1407
+ } catch {
1408
+ return decision;
1274
1409
  }
1275
- return undefined;
1276
1410
  });
1277
1411
  } catch {
1278
1412
  // event bus missing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roarpeng/graphflow",
3
- "version": "1.14.0",
3
+ "version": "1.15.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -31,7 +31,7 @@
31
31
  "node": ">=20",
32
32
  "npm": ">=10"
33
33
  },
34
- "description": "A Context-Aware Multi-Agent Orchestration Engine",
34
+ "description": "Local-first memory & context harness for coding agents",
35
35
  "main": "dist/index.js",
36
36
  "types": "dist/index.d.ts",
37
37
  "exports": {
@@ -98,11 +98,12 @@
98
98
  "url": "git+https://github.com/Roarpeng/GraphFlow.git"
99
99
  },
100
100
  "keywords": [
101
- "multi-agent",
102
- "orchestration",
101
+ "memory-harness",
102
+ "context-harness",
103
+ "local-first",
103
104
  "mcp",
104
- "context-engine",
105
- "agent-plugins",
105
+ "knowledge-graph",
106
+ "coding-agents",
106
107
  "cursor",
107
108
  "claude-code",
108
109
  "dsh-plugin",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "graphflow",
4
- "version": "1.14.0",
4
+ "version": "1.15.0",
5
5
  "description": "图谱上下文压缩与任务规划插件(10 个 MCP 工具)。能力:压缩上下文、DAG 规划、增量建图、技能飞轮、诊断与产物导入导出。DeepSeek Harness:dsh plugin --profile web add @roarpeng/graphflow;工具名为 mcp__graphflow__graphflow_*。先调 graphflow_context(传 rootDir),复杂任务再 plan,改完代码后 index。",
6
6
  "author": {
7
7
  "name": "Roarpeng",