snow-flow 10.0.200 → 10.0.201

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
- "version": "10.0.200",
3
+ "version": "10.0.201",
4
4
  "name": "snow-flow",
5
5
  "description": "Snow-Flow - ServiceNow Multi-Agent Development Framework powered by AI",
6
6
  "license": "Elastic-2.0",
@@ -28,6 +28,7 @@ import { Truncate } from "./truncation"
28
28
  import { PlanExitTool, PlanEnterTool } from "./plan"
29
29
  import { ReviewEnterTool, ReviewExitTool } from "./review"
30
30
  import { ApplyPatchTool } from "./apply_patch"
31
+ import { Auth } from "@/auth"
31
32
 
32
33
  export namespace ToolRegistry {
33
34
  const log = Log.create({ service: "tool.registry" })
@@ -99,6 +100,9 @@ export namespace ToolRegistry {
99
100
  const custom = await state().then((x) => x.custom)
100
101
  const config = await Config.get()
101
102
 
103
+ // Check if the user has the code-review feature (enterprise subscription)
104
+ const hasCodeReview = await hasFeature("code-review")
105
+
102
106
  return [
103
107
  InvalidTool,
104
108
  ...(["app", "cli", "desktop"].includes(Flag.OPENCODE_CLIENT) ? [QuestionTool] : []),
@@ -118,11 +122,23 @@ export namespace ToolRegistry {
118
122
  ApplyPatchTool,
119
123
  ...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [LspTool] : []),
120
124
  ...(config.experimental?.batch_tool === true ? [BatchTool] : []),
121
- ...(Flag.OPENCODE_CLIENT === "cli" ? [PlanExitTool, PlanEnterTool, ReviewEnterTool, ReviewExitTool] : []),
125
+ ...(Flag.OPENCODE_CLIENT === "cli" ? [PlanExitTool, PlanEnterTool] : []),
126
+ ...(Flag.OPENCODE_CLIENT === "cli" && hasCodeReview ? [ReviewEnterTool, ReviewExitTool] : []),
122
127
  ...custom,
123
128
  ]
124
129
  }
125
130
 
131
+ /** Check if the user has a specific feature in their enterprise subscription */
132
+ async function hasFeature(feature: string): Promise<boolean> {
133
+ try {
134
+ const auths = await Auth.all()
135
+ for (const auth of Object.values(auths)) {
136
+ if (auth.type === "enterprise" && auth.features?.includes(feature)) return true
137
+ }
138
+ } catch {}
139
+ return false
140
+ }
141
+
126
142
  export async function ids() {
127
143
  return all().then((x) => x.map((t) => t.id))
128
144
  }
@@ -28,6 +28,19 @@ function getMachineId(): string | undefined {
28
28
  }
29
29
  }
30
30
 
31
+ function detectInstallMethod(): string {
32
+ // Check npm
33
+ if (process.env.npm_config_user_agent) return "npm"
34
+ // Check bun
35
+ if (typeof globalThis.Bun !== "undefined") return "bun"
36
+ // Check homebrew
37
+ const execPath = process.execPath || ""
38
+ if (execPath.includes("Cellar") || execPath.includes("homebrew") || execPath.includes("linuxbrew")) return "brew"
39
+ // Check standalone binary (no node_modules in path suggests binary distribution)
40
+ if (!execPath.includes("node_modules") && !execPath.includes("node") && !execPath.includes("bun")) return "binary"
41
+ return "other"
42
+ }
43
+
31
44
  async function sendPing(payload: Record<string, unknown>): Promise<void> {
32
45
  try {
33
46
  const response = await fetch(`${PORTAL_URL}/api/telemetry/ping`, {
@@ -63,9 +76,11 @@ export namespace AnonymousTelemetry {
63
76
  }
64
77
 
65
78
  let messageCount = 0
79
+ let exitReason: "normal" | "error" | "interrupt" = "normal"
66
80
  let configDisabled = false
67
81
  const startTime = Date.now()
68
82
  const sessionId = crypto.randomUUID()
83
+ const installMethod = detectInstallMethod()
69
84
 
70
85
  const unsubs = [
71
86
  Bus.subscribe(MessageV2.Event.Updated, (event) => {
@@ -73,6 +88,18 @@ export namespace AnonymousTelemetry {
73
88
  }),
74
89
  ]
75
90
 
91
+ // Track exit reason via process signals
92
+ const onError = () => {
93
+ exitReason = "error"
94
+ }
95
+ const onInterrupt = () => {
96
+ exitReason = "interrupt"
97
+ }
98
+ process.on("uncaughtException", onError)
99
+ process.on("unhandledRejection", onError)
100
+ process.on("SIGINT", onInterrupt)
101
+ process.on("SIGTERM", onInterrupt)
102
+
76
103
  const basePayload = {
77
104
  machineId,
78
105
  sessionId,
@@ -80,6 +107,7 @@ export namespace AnonymousTelemetry {
80
107
  channel: Installation.CHANNEL,
81
108
  os: process.platform,
82
109
  arch: process.arch,
110
+ installMethod,
83
111
  }
84
112
 
85
113
  log.info("anonymous telemetry initializing", { machineId: machineId.slice(0, 8) + "..." })
@@ -111,20 +139,32 @@ export namespace AnonymousTelemetry {
111
139
  get messageCount() {
112
140
  return messageCount
113
141
  },
142
+ get exitReason() {
143
+ return exitReason
144
+ },
114
145
  get configDisabled() {
115
146
  return configDisabled
116
147
  },
148
+ cleanup() {
149
+ process.removeListener("uncaughtException", onError)
150
+ process.removeListener("unhandledRejection", onError)
151
+ process.removeListener("SIGINT", onInterrupt)
152
+ process.removeListener("SIGTERM", onInterrupt)
153
+ },
117
154
  }
118
155
  },
119
156
  async (current) => {
120
157
  for (const unsub of current.unsubs) unsub()
121
158
  if (current.disabled || current.configDisabled) return
122
159
 
160
+ if ("cleanup" in current) current.cleanup()
161
+
123
162
  const sessionDurationSec = Math.round((Date.now() - current.startTime) / 1000)
124
163
 
125
164
  await sendPing({
126
165
  ...current.basePayload,
127
166
  type: "end",
167
+ exitReason: current.exitReason,
128
168
  sessionDurationSec,
129
169
  messageCount: current.messageCount,
130
170
  timestamp: Date.now(),